neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
Log in to mark this repository.
Credo: complex functions split into tables and named helpers
f611475 · Gabe Morell · 2026-11-04 20:49
Message
{commit_body(@commit)}
Files changed
modified
lib/git_gud/outbound_url.ex
+15
−11
| 254 | 254 | Whether `ip` (an `:inet` tuple) is somewhere a user-chosen URL must |
| 255 | 255 | not reach. |
| 256 | 256 | """ |
| 257 | − def blocked?({a, b, _c, _d} = ip) when tuple_size(ip) == 4 do | |
| 258 | − a == 0 or a == 10 or a == 127 or a >= 224 or | |
| 259 | − (a == 100 and (b &&& 0xC0) == 64) or | |
| 260 | − (a == 169 and b == 254) or | |
| 261 | − (a == 172 and (b &&& 0xF0) == 16) or | |
| 262 | − (a == 192 and b == 168) or | |
| 263 | − (a == 192 and b == 0 and elem(ip, 2) in [0, 2]) or | |
| 264 | − (a == 198 and b in [18, 19]) or | |
| 265 | − (a == 198 and b == 51 and elem(ip, 2) == 100) or | |
| 266 | − (a == 203 and b == 0 and elem(ip, 2) == 113) | |
| 267 | − end | |
| 257 | + def blocked?({a, b, c, _d} = ip) when tuple_size(ip) == 4, do: blocked_v4?(a, b, c) | |
| 268 | 258 | |
| 259 | + # The v6 ranges, one per line, read best as a single table. | |
| 260 | + # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity | |
| 269 | 261 | def blocked?({a, b, c, d, e, f, g, h} = ip) when tuple_size(ip) == 8 do |
| 270 | 262 | cond do |
| 271 | 263 | # :: and ::1 |
| 288 | 280 | |
| 289 | 281 | def blocked?(_), do: true |
| 290 | 282 | |
| 283 | + # By the first three octets. | |
| 284 | + defp blocked_v4?(a, _b, _c) when a in [0, 10, 127] or a >= 224, do: true | |
| 285 | + defp blocked_v4?(100, b, _c), do: (b &&& 0xC0) == 64 | |
| 286 | + defp blocked_v4?(169, 254, _c), do: true | |
| 287 | + defp blocked_v4?(172, b, _c), do: (b &&& 0xF0) == 16 | |
| 288 | + defp blocked_v4?(192, 168, _c), do: true | |
| 289 | + defp blocked_v4?(192, 0, c), do: c in [0, 2] | |
| 290 | + defp blocked_v4?(198, b, _c) when b in [18, 19], do: true | |
| 291 | + defp blocked_v4?(198, 51, 100), do: true | |
| 292 | + defp blocked_v4?(203, 0, 113), do: true | |
| 293 | + defp blocked_v4?(_a, _b, _c), do: false | |
| 294 | + | |
| 291 | 295 | defp v4(hi, lo), do: {hi >>> 8, hi &&& 0xFF, lo >>> 8, lo &&& 0xFF} |
| 292 | 296 | |
| 293 | 297 | defp ip_string(ip), do: ip |> :inet.ntoa() |> to_string() |
modified
lib/git_gud/packages/formats/raw.ex
+13
−11
| 77 | 77 | # The version slot that means "resolve it for me". |
| 78 | 78 | @latest "latest" |
| 79 | 79 | |
| 80 | + @content_types %{ | |
| 81 | + ".json" => "application/json", | |
| 82 | + ".txt" => "text/plain; charset=utf-8", | |
| 83 | + ".md" => "text/markdown; charset=utf-8", | |
| 84 | + ".sha256" => "text/plain; charset=utf-8", | |
| 85 | + ".csv" => "text/csv; charset=utf-8", | |
| 86 | + ".gz" => "application/gzip", | |
| 87 | + ".zip" => "application/zip", | |
| 88 | + ".tar" => "application/x-tar" | |
| 89 | + } | |
| 90 | + | |
| 80 | 91 | @doc """ |
| 81 | 92 | Media type for a stored artifact. |
| 82 | 93 |
| 87 | 98 | """ |
| 88 | 99 | @impl true |
| 89 | 100 | def content_type(filename) when is_binary(filename) do |
| 90 | − case filename |> Path.extname() |> String.downcase() do | |
| 91 | − ".json" -> "application/json" | |
| 92 | − ".txt" -> "text/plain; charset=utf-8" | |
| 93 | − ".md" -> "text/markdown; charset=utf-8" | |
| 94 | − ".sha256" -> "text/plain; charset=utf-8" | |
| 95 | − ".csv" -> "text/csv; charset=utf-8" | |
| 96 | − ".gz" -> "application/gzip" | |
| 97 | − ".zip" -> "application/zip" | |
| 98 | − ".tar" -> "application/x-tar" | |
| 99 | − _ -> "application/octet-stream" | |
| 100 | − end | |
| 101 | + ext = filename |> Path.extname() |> String.downcase() | |
| 102 | + Map.get(@content_types, ext, "application/octet-stream") | |
| 101 | 103 | end |
| 102 | 104 | |
| 103 | 105 | def content_type(_filename), do: "application/octet-stream" |
modified
lib/git_gud/reports.ex
+23
−21
| 329 | 329 | kind: :issue, |
| 330 | 330 | title: "##{issue.number} #{issue.title}", |
| 331 | 331 | excerpt: excerpt(issue.original_body || issue.body), |
| 332 | − url: repo && "/r/#{handle(repo)}/#{repo.name}/issues/#{issue.number}", | |
| 332 | + url: item_url(repo, "issues", issue.number), | |
| 333 | 333 | author: user_name(author), |
| 334 | 334 | author_id: author && author.id, |
| 335 | 335 | moderatable?: true, |
| 351 | 351 | |
| 352 | 352 | %{ |
| 353 | 353 | kind: :issue_comment, |
| 354 | − title: | |
| 355 | − "comment on ##{issue && issue.number} #{(issue && issue.title) || "(deleted issue)"}", | |
| 354 | + title: comment_title(issue, "(deleted issue)"), | |
| 356 | 355 | excerpt: excerpt(comment.original_body || comment.body), |
| 357 | − url: repo && issue && "/r/#{handle(repo)}/#{repo.name}/issues/#{issue.number}", | |
| 356 | + url: item_url(repo, "issues", issue && issue.number), | |
| 358 | 357 | author: user_name(author), |
| 359 | 358 | author_id: author && author.id, |
| 360 | 359 | moderatable?: true, |
| 378 | 377 | kind: :pull_request, |
| 379 | 378 | title: "##{pr.number} #{pr.title}", |
| 380 | 379 | excerpt: excerpt(pr.original_body || pr.body), |
| 381 | − url: repo && "/r/#{handle(repo)}/#{repo.name}/pulls/#{pr.number}", | |
| 380 | + url: item_url(repo, "pulls", pr.number), | |
| 382 | 381 | author_id: author && author.id, |
| 383 | − author: | |
| 384 | − cond do | |
| 385 | − author -> user_name(author) | |
| 386 | − source_actor -> actor_name(source_actor) | |
| 387 | − true -> "(unknown)" | |
| 388 | − end, | |
| 382 | + author: author_label(author, source_actor), | |
| 389 | 383 | moderatable?: true, |
| 390 | 384 | moderated?: GitGud.PullRequests.PullRequest.moderated?(pr), |
| 391 | − suspendable_actor_id: source_actor && source_actor.kind == "remote" && source_actor.id | |
| 385 | + suspendable_actor_id: remote_actor_id(source_actor) | |
| 392 | 386 | } |
| 393 | 387 | end |
| 394 | 388 | end |
| 408 | 402 | |
| 409 | 403 | %{ |
| 410 | 404 | kind: :pr_comment, |
| 411 | − title: "comment on ##{pr && pr.number} #{(pr && pr.title) || "(deleted PR)"}", | |
| 405 | + title: comment_title(pr, "(deleted PR)"), | |
| 412 | 406 | excerpt: excerpt(comment.original_body || comment.body), |
| 413 | − url: repo && pr && "/r/#{handle(repo)}/#{repo.name}/pulls/#{pr.number}", | |
| 407 | + url: item_url(repo, "pulls", pr && pr.number), | |
| 414 | 408 | author_id: author && author.id, |
| 415 | − author: | |
| 416 | − cond do | |
| 417 | − author -> user_name(author) | |
| 418 | − source_actor -> actor_name(source_actor) | |
| 419 | − true -> "(unknown)" | |
| 420 | − end, | |
| 409 | + author: author_label(author, source_actor), | |
| 421 | 410 | moderatable?: true, |
| 422 | 411 | moderated?: GitGud.PullRequests.PrComment.moderated?(comment), |
| 423 | − suspendable_actor_id: source_actor && source_actor.kind == "remote" && source_actor.id | |
| 412 | + suspendable_actor_id: remote_actor_id(source_actor) | |
| 424 | 413 | } |
| 425 | 414 | end |
| 426 | 415 | end |
| 465 | 454 | |
| 466 | 455 | defp build_preview(_, _), do: :missing |
| 467 | 456 | |
| 457 | + defp comment_title(item, missing), | |
| 458 | + do: "comment on ##{item && item.number} #{(item && item.title) || missing}" | |
| 459 | + | |
| 460 | + defp item_url(nil, _path, _number), do: nil | |
| 461 | + defp item_url(repo, path, number), do: "/r/#{handle(repo)}/#{repo.name}/#{path}/#{number}" | |
| 462 | + | |
| 463 | + defp author_label(nil, nil), do: "(unknown)" | |
| 464 | + defp author_label(nil, source_actor), do: actor_name(source_actor) | |
| 465 | + defp author_label(author, _source_actor), do: user_name(author) | |
| 466 | + | |
| 467 | + defp remote_actor_id(source_actor), | |
| 468 | + do: source_actor && source_actor.kind == "remote" && source_actor.id | |
| 469 | + | |
| 468 | 470 | defp repo_for_target_repo_id(rid) do |
| 469 | 471 | case Repo.get(GitGud.Repositories.Repository, rid) do |
| 470 | 472 | nil -> nil |
modified
lib/git_gud/workflows.ex
+85
−62
| 934 | 934 | # Cancelled once this run is committed, rather than from inside its |
| 935 | 935 | # transaction: `cancel/2` releases each one's group, and that |
| 936 | 936 | # should see this run. |
| 937 | − for id <- hold.cancel do | |
| 938 | − case Repo.get(WorkflowRun, id) do | |
| 939 | − %WorkflowRun{} = superseded -> cancel(superseded, reason: hold.cancel_reason) | |
| 940 | − nil -> :ok | |
| 941 | − end | |
| 942 | − end | |
| 937 | + cancel_superseded(hold) | |
| 943 | 938 | |
| 944 | 939 | {:ok, run} |
| 945 | 940 |
| 948 | 943 | end |
| 949 | 944 | end |
| 950 | 945 | |
| 946 | + defp cancel_superseded(hold) do | |
| 947 | + for id <- hold.cancel do | |
| 948 | + case Repo.get(WorkflowRun, id) do | |
| 949 | + %WorkflowRun{} = superseded -> cancel(superseded, reason: hold.cancel_reason) | |
| 950 | + nil -> :ok | |
| 951 | + end | |
| 952 | + end | |
| 953 | + end | |
| 954 | + | |
| 951 | 955 | # The workflow's `concurrency:`, decided as the run is created (GitHub |
| 952 | 956 | # decides a workflow's group then, so it can't read `matrix` or `needs`). |
| 953 | 957 | # Returns the run, with its group recorded, and what to do about it: |
| 2223 | 2227 | |
| 2224 | 2228 | locked |
| 2225 | 2229 | |> WorkflowJob.status_changeset(concluded, opts) |
| 2226 | − |> then(fn cs -> | |
| 2227 | − case reason || why do | |
| 2228 | − nil -> cs | |
| 2229 | − reason -> Ecto.Changeset.change(cs, status_reason: reason) | |
| 2230 | − end | |
| 2231 | − end) | |
| 2232 | − |> then(fn cs -> | |
| 2233 | − if is_nil(from) or locked.status in from, | |
| 2234 | − do: cs, | |
| 2235 | − else: Ecto.Changeset.add_error(cs, :status, "has moved on", from: locked.status) | |
| 2236 | − end) | |
| 2230 | + |> put_status_reason(reason || why) | |
| 2231 | + |> require_status_from(from, locked.status) | |
| 2237 | 2232 | |> Repo.update() |
| 2238 | 2233 | end) |
| 2239 | 2234 |
| 2257 | 2252 | end |
| 2258 | 2253 | end |
| 2259 | 2254 | |
| 2255 | + defp put_status_reason(cs, nil), do: cs | |
| 2256 | + defp put_status_reason(cs, reason), do: Ecto.Changeset.change(cs, status_reason: reason) | |
| 2257 | + | |
| 2258 | + defp require_status_from(cs, nil, _current), do: cs | |
| 2259 | + | |
| 2260 | + defp require_status_from(cs, from, current) do | |
| 2261 | + if current in from, | |
| 2262 | + do: cs, | |
| 2263 | + else: Ecto.Changeset.add_error(cs, :status, "has moved on", from: current) | |
| 2264 | + end | |
| 2265 | + | |
| 2260 | 2266 | # ── concurrency: jobs ──────────────────────────────────────────────── |
| 2261 | 2267 | |
| 2262 | 2268 | # The `status_reason` of a job cancelled by a newer one in its group. |
| 2358 | 2364 | ) |
| 2359 | 2365 | |
| 2360 | 2366 | {:ok, group, cancel?} -> |
| 2361 | − {:ok, row} = | |
| 2362 | − Repo.transaction(fn -> | |
| 2363 | − Concurrency.lock!(run.repository_id) | |
| 2364 | − members = job_group_members(run.repository_id, group, job.id) | |
| 2365 | − wait? = not cancel? and Enum.any?(members, &(&1.status in ["queued", "running"])) | |
| 2366 | − | |
| 2367 | − row = | |
| 2368 | − put_job( | |
| 2369 | − job, | |
| 2370 | − changes ++ | |
| 2371 | − [ | |
| 2372 | − status: if(wait?, do: "waiting", else: "queued"), | |
| 2373 | − concurrency_group: group, | |
| 2374 | − cancel_in_progress: cancel?, | |
| 2375 | − status_reason: | |
| 2376 | − if(wait?, do: "Waiting for " <> group, else: next.status_reason) | |
| 2377 | − ] | |
| 2378 | − ) | |
| 2367 | + join_job_group(job, next, run, changes, group, cancel?) | |
| 2368 | + end | |
| 2369 | + end | |
| 2379 | 2370 | |
| 2380 | − superseded = | |
| 2381 | − if cancel?, do: members, else: Enum.filter(members, &(&1.status == "waiting")) | |
| 2371 | + defp join_job_group(job, next, run, changes, group, cancel?) do | |
| 2372 | + {:ok, row} = | |
| 2373 | + Repo.transaction(fn -> | |
| 2374 | + Concurrency.lock!(run.repository_id) | |
| 2375 | + members = job_group_members(run.repository_id, group, job.id) | |
| 2376 | + wait? = not cancel? and Enum.any?(members, &(&1.status in ["queued", "running"])) | |
| 2377 | + | |
| 2378 | + row = | |
| 2379 | + put_job( | |
| 2380 | + job, | |
| 2381 | + changes ++ | |
| 2382 | + [ | |
| 2383 | + status: if(wait?, do: "waiting", else: "queued"), | |
| 2384 | + concurrency_group: group, | |
| 2385 | + cancel_in_progress: cancel?, | |
| 2386 | + status_reason: if(wait?, do: "Waiting for " <> group, else: next.status_reason) | |
| 2387 | + ] | |
| 2388 | + ) | |
| 2382 | 2389 | |
| 2383 | − for member <- superseded do | |
| 2384 | − update_job_status(member, "cancelled", | |
| 2385 | − reason: @superseded <> group, | |
| 2386 | − fail_fast: false | |
| 2387 | − ) | |
| 2388 | − end | |
| 2390 | + superseded = | |
| 2391 | + if cancel?, do: members, else: Enum.filter(members, &(&1.status == "waiting")) | |
| 2389 | 2392 | |
| 2390 | − row | |
| 2391 | − end) | |
| 2393 | + for member <- superseded do | |
| 2394 | + update_job_status(member, "cancelled", | |
| 2395 | + reason: @superseded <> group, | |
| 2396 | + fail_fast: false | |
| 2397 | + ) | |
| 2398 | + end | |
| 2392 | 2399 | |
| 2393 | 2400 | row |
| 2394 | − end | |
| 2401 | + end) | |
| 2402 | + | |
| 2403 | + row | |
| 2395 | 2404 | end |
| 2396 | 2405 | |
| 2397 | 2406 | defp put_job(%WorkflowJob{} = job, fields) do |
| 3004 | 3013 | defp superseded_by_group?(%WorkflowJob{status_reason: @run_superseded <> _}), do: true |
| 3005 | 3014 | defp superseded_by_group?(job), do: superseded?(job) |
| 3006 | 3015 | |
| 3007 | − defp job_status_description(%WorkflowJob{} = job) do | |
| 3008 | − case job.status do | |
| 3009 | − "queued" -> "Waiting to run" | |
| 3010 | − "blocked" -> "Waiting for " <> Enum.join(job.needs, ", ") | |
| 3011 | − "waiting" -> job.status_reason || "Waiting" | |
| 3012 | − "awaiting_approval" -> "Waiting for approval" | |
| 3013 | − "running" -> "Running" | |
| 3014 | − "success" -> "Successful" <> took(job, " in ") | |
| 3015 | − "failure" when is_nil(job.started_at) -> job.status_reason || "Failing" | |
| 3016 | − "failure" -> "Failing" <> took(job, " after ") | |
| 3017 | − "warning" -> warning_description(job) | |
| 3018 | − "cancelled" -> job.status_reason || "Cancelled" | |
| 3019 | − "skipped" -> "Skipped" | |
| 3020 | − other -> other | |
| 3021 | − end | |
| 3022 | − end | |
| 3016 | + defp job_status_description(%WorkflowJob{status: "queued"}), do: "Waiting to run" | |
| 3017 | + | |
| 3018 | + defp job_status_description(%WorkflowJob{status: "blocked"} = job), | |
| 3019 | + do: "Waiting for " <> Enum.join(job.needs, ", ") | |
| 3020 | + | |
| 3021 | + defp job_status_description(%WorkflowJob{status: "waiting"} = job), | |
| 3022 | + do: job.status_reason || "Waiting" | |
| 3023 | + | |
| 3024 | + defp job_status_description(%WorkflowJob{status: "awaiting_approval"}), | |
| 3025 | + do: "Waiting for approval" | |
| 3026 | + | |
| 3027 | + defp job_status_description(%WorkflowJob{status: "running"}), do: "Running" | |
| 3028 | + | |
| 3029 | + defp job_status_description(%WorkflowJob{status: "success"} = job), | |
| 3030 | + do: "Successful" <> took(job, " in ") | |
| 3031 | + | |
| 3032 | + defp job_status_description(%WorkflowJob{status: "failure", started_at: nil} = job), | |
| 3033 | + do: job.status_reason || "Failing" | |
| 3034 | + | |
| 3035 | + defp job_status_description(%WorkflowJob{status: "failure"} = job), | |
| 3036 | + do: "Failing" <> took(job, " after ") | |
| 3037 | + | |
| 3038 | + defp job_status_description(%WorkflowJob{status: "warning"} = job), | |
| 3039 | + do: warning_description(job) | |
| 3040 | + | |
| 3041 | + defp job_status_description(%WorkflowJob{status: "cancelled"} = job), | |
| 3042 | + do: job.status_reason || "Cancelled" | |
| 3043 | + | |
| 3044 | + defp job_status_description(%WorkflowJob{status: "skipped"}), do: "Skipped" | |
| 3045 | + defp job_status_description(%WorkflowJob{status: other}), do: other | |
| 3023 | 3046 | |
| 3024 | 3047 | defp warning_description(%WorkflowJob{} = job) do |
| 3025 | 3048 | if continue_on_error?(job), |
modified
lib/git_gud/workflows/context.ex
+20
−8
| 71 | 71 | # "Username required". |
| 72 | 72 | owner_id = repo.organization_id || repo.owner_id |
| 73 | 73 | |
| 74 | − {actor, actor_id} = | |
| 75 | − case run.triggered_by_id && Repo.get(User, run.triggered_by_id) do | |
| 76 | − %User{handle: h, id: id} when is_binary(h) and h != "" -> {h, id} | |
| 77 | − _ -> {owner, owner_id} | |
| 78 | − end | |
| 74 | + {actor, actor_id} = triggering_actor(run, owner, owner_id) | |
| 79 | 75 | |
| 80 | 76 | full_name = owner <> "/" <> repo.name |
| 81 | 77 |
| 99 | 95 | # expensive kind. |
| 100 | 96 | "ref_name" => Ref.name(run.head_ref), |
| 101 | 97 | "ref_type" => Ref.type(run.head_ref), |
| 102 | − "sha" => if(run.head_sha, do: Git.to_hex(run.head_sha), else: ""), | |
| 98 | + "sha" => sha_hex(run.head_sha), | |
| 103 | 99 | "repository" => full_name, |
| 104 | 100 | "repository_id" => Integer.to_string(repo.id), |
| 105 | 101 | "repository_owner" => owner, |
| 111 | 107 | # endpoints check against the token's run. |
| 112 | 108 | "run_id" => Integer.to_string(run.id), |
| 113 | 109 | "run_number" => maybe_int_to_string(run.number), |
| 114 | − "run_attempt" => maybe_int_to_string((job && job.attempt) || 1), | |
| 110 | + "run_attempt" => maybe_int_to_string(job_attempt(job)), | |
| 115 | 111 | "workflow_ref" => full_name <> "/" <> run.workflow_path <> "@" <> (run.head_ref || ""), |
| 116 | 112 | "server_url" => base, |
| 117 | 113 | "api_url" => base <> "/api/v1", |
| 118 | 114 | # Forgejo sends it empty too: artifacts follow the instance's policy. |
| 119 | 115 | "retention_days" => "", |
| 120 | 116 | "workflow" => run.workflow_name || run.workflow_path, |
| 121 | − "job" => (job && job.job_id) || "", | |
| 117 | + "job" => job_key(job), | |
| 122 | 118 | # `gitea_default_actions_url` tells the runner where to clone |
| 123 | 119 | # `uses: actions/<repo>@<ref>` from. If we point at our own |
| 124 | 120 | # instance, the runner expects `actions/checkout` etc. to be |
| 134 | 130 | |> Map.merge(pull_request_context(run)) |
| 135 | 131 | end |
| 136 | 132 | |
| 133 | + defp triggering_actor(run, owner, owner_id) do | |
| 134 | + case run.triggered_by_id && Repo.get(User, run.triggered_by_id) do | |
| 135 | + %User{handle: h, id: id} when is_binary(h) and h != "" -> {h, id} | |
| 136 | + _ -> {owner, owner_id} | |
| 137 | + end | |
| 138 | + end | |
| 139 | + | |
| 140 | + defp sha_hex(nil), do: "" | |
| 141 | + defp sha_hex(sha), do: Git.to_hex(sha) | |
| 142 | + | |
| 143 | + defp job_attempt(nil), do: 1 | |
| 144 | + defp job_attempt(job), do: job.attempt || 1 | |
| 145 | + | |
| 146 | + defp job_key(nil), do: "" | |
| 147 | + defp job_key(job), do: job.job_id || "" | |
| 148 | + | |
| 137 | 149 | # What GitHub gives a `pull_request` job. `ref` is already the run's |
| 138 | 150 | # `head_ref` (`refs/pull/<N>/head`); its short name is `<N>/head`, and |
| 139 | 151 | # `ref_type` is empty, as on GitHub — a PR ref is neither a branch nor a |
modified
lib/git_gud/workflows/runner_jwt.ex
+19
−15
Click to load diff…
modified
lib/git_gud_web/components/audit_components.ex
+2
−0
Click to load diff…
modified
lib/git_gud_web/controllers/actions_cache_controller.ex
+16
−22
Click to load diff…
modified
lib/git_gud_web/controllers/ap_actor_controller.ex
+25
−23
Click to load diff…
modified
lib/git_gud_web/controllers/api/mobile/ci_controller.ex
+36
−38
Click to load diff…
modified
lib/git_gud_web/controllers/package_registry_controller.ex
+22
−27
Click to load diff…
modified
lib/git_gud_web/controllers/packages_webhook_controller.ex
+3
−2
Click to load diff…
modified
lib/git_gud_web/live/audit_live/filters.ex
+11
−8
Click to load diff…
modified
lib/git_gud_web/live/repo_live/new.ex
+12
−14
Click to load diff…
Parents: 591e34a