neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
Log in to mark this repository.
Credo: deep nesting out, into named private functions
591e34a · Gabe Morell · 2026-11-04 20:41
Message
{commit_body(@commit)}
Files changed
modified
lib/git_gud/accounts/user.ex
+11
−22
| 119 | 119 | # email local-part. Collisions are caught by the unique constraint; |
| 120 | 120 | # callers can retry with a suffix. |
| 121 | 121 | defp maybe_seed_handle(changeset) do |
| 122 | − case get_field(changeset, :handle) do | |
| 123 | − nil -> | |
| 124 | − email = get_field(changeset, :email) | |
| 125 | − | |
| 126 | − if is_binary(email) do | |
| 127 | − base = email |> String.split("@") |> hd() |> String.downcase() | |
| 128 | − | |
| 129 | − if String.match?(base, @handle_format) do | |
| 130 | − # The CI account's handle is the forge's even before that | |
| 131 | − # account exists: someone registering as gg-security@… is | |
| 132 | − # gg-security_1, as for a handle already taken. | |
| 133 | − base = if base == GitGud.Accounts.ci_handle(), do: base <> "_1", else: base | |
| 134 | − put_change(changeset, :handle, base) | |
| 135 | − else | |
| 136 | − changeset | |
| 137 | − end | |
| 138 | − else | |
| 139 | − changeset | |
| 140 | − end | |
| 141 | − | |
| 142 | − _existing -> | |
| 143 | − changeset | |
| 122 | + with nil <- get_field(changeset, :handle), | |
| 123 | + email when is_binary(email) <- get_field(changeset, :email), | |
| 124 | + base = email |> String.split("@") |> hd() |> String.downcase(), | |
| 125 | + true <- String.match?(base, @handle_format) do | |
| 126 | + # The CI account's handle is the forge's even before that | |
| 127 | + # account exists: someone registering as gg-security@… is | |
| 128 | + # gg-security_1, as for a handle already taken. | |
| 129 | + base = if base == GitGud.Accounts.ci_handle(), do: base <> "_1", else: base | |
| 130 | + put_change(changeset, :handle, base) | |
| 131 | + else | |
| 132 | + _ -> changeset | |
| 144 | 133 | end |
| 145 | 134 | end |
| 146 | 135 |
modified
lib/git_gud/audit.ex
+44
−48
| 118 | 118 | end |
| 119 | 119 | |
| 120 | 120 | result = |
| 121 | − Repo.transact(fn -> | |
| 122 | − case Keyword.get(opts, :record, :after) do | |
| 123 | − :before -> | |
| 124 | − entries = insert_all!.(:before) | |
| 125 | − wrap(fun.(), entries) | |
| 126 | − | |
| 127 | − :after -> | |
| 128 | − case fun.() do | |
| 129 | − ok when ok == :ok or (is_tuple(ok) and elem(ok, 0) == :ok) -> | |
| 130 | − value = if ok == :ok, do: :ok, else: elem(ok, 1) | |
| 131 | − {:ok, {ok, insert_all!.(value)}} | |
| 132 | − | |
| 133 | − other -> | |
| 134 | − {:error, {:returned, other}} | |
| 135 | − end | |
| 136 | − end | |
| 137 | − end) | |
| 121 | + Repo.transact(fn -> run_recorded(Keyword.get(opts, :record, :after), fun, insert_all!) end) | |
| 138 | 122 | |
| 139 | 123 | case result do |
| 140 | 124 | {:ok, {original, entries}} -> |
| 146 | 130 | end |
| 147 | 131 | end |
| 148 | 132 | |
| 133 | + defp run_recorded(:before, fun, insert_all!) do | |
| 134 | + entries = insert_all!.(:before) | |
| 135 | + wrap(fun.(), entries) | |
| 136 | + end | |
| 137 | + | |
| 138 | + defp run_recorded(:after, fun, insert_all!) do | |
| 139 | + case fun.() do | |
| 140 | + ok when ok == :ok or (is_tuple(ok) and elem(ok, 0) == :ok) -> | |
| 141 | + value = if ok == :ok, do: :ok, else: elem(ok, 1) | |
| 142 | + {:ok, {ok, insert_all!.(value)}} | |
| 143 | + | |
| 144 | + other -> | |
| 145 | + {:error, {:returned, other}} | |
| 146 | + end | |
| 147 | + end | |
| 148 | + | |
| 149 | 149 | defp wrap(result, entries) do |
| 150 | 150 | if result == :ok or (is_tuple(result) and elem(result, 0) == :ok), |
| 151 | 151 | do: {:ok, {result, entries}}, |
| 386 | 386 | |> scoped(scope) |
| 387 | 387 | |> filtered(filters) |
| 388 | 388 | |
| 389 | − {rows, more?} = | |
| 390 | − case cursor do | |
| 391 | − {:after, id} -> | |
| 392 | − rows = | |
| 393 | − from(e in base, where: e.id > ^id, order_by: [asc: e.id], limit: ^(limit + 1)) | |
| 394 | − |> Repo.all() | |
| 389 | + {rows, more?} = fetch_page(base, cursor, limit) | |
| 390 | + rows = Repo.preload(rows, [:actor]) | |
| 395 | 391 | |
| 396 | − {Enum.reverse(Enum.take(rows, limit)), length(rows) > limit} | |
| 392 | + {rows, page_cursors(cursor, rows, more?)} | |
| 393 | + end | |
| 397 | 394 | |
| 398 | − before -> | |
| 399 | − query = from(e in base, order_by: [desc: e.id], limit: ^(limit + 1)) | |
| 395 | + defp fetch_page(base, {:after, id}, limit) do | |
| 396 | + rows = | |
| 397 | + from(e in base, where: e.id > ^id, order_by: [asc: e.id], limit: ^(limit + 1)) | |
| 398 | + |> Repo.all() | |
| 400 | 399 | |
| 401 | − query = | |
| 402 | − case before do | |
| 403 | − {:before, id} -> from(e in query, where: e.id < ^id) | |
| 404 | − _ -> query | |
| 405 | − end | |
| 400 | + {Enum.reverse(Enum.take(rows, limit)), length(rows) > limit} | |
| 401 | + end | |
| 406 | 402 | |
| 407 | − rows = Repo.all(query) | |
| 408 | − {Enum.take(rows, limit), length(rows) > limit} | |
| 409 | − end | |
| 403 | + defp fetch_page(base, before, limit) do | |
| 404 | + query = from(e in base, order_by: [desc: e.id], limit: ^(limit + 1)) | |
| 410 | 405 | |
| 411 | − rows = Repo.preload(rows, [:actor]) | |
| 406 | + query = | |
| 407 | + case before do | |
| 408 | + {:before, id} -> from(e in query, where: e.id < ^id) | |
| 409 | + _ -> query | |
| 410 | + end | |
| 412 | 411 | |
| 413 | − cursors = | |
| 414 | − case {cursor, rows} do | |
| 415 | − {_, []} -> | |
| 416 | − %{older: nil, newer: nil} | |
| 412 | + rows = Repo.all(query) | |
| 413 | + {Enum.take(rows, limit), length(rows) > limit} | |
| 414 | + end | |
| 417 | 415 | |
| 418 | − {{:after, _}, rows} -> | |
| 419 | − %{older: List.last(rows).id, newer: if(more?, do: hd(rows).id)} | |
| 416 | + defp page_cursors(_cursor, [], _more?), do: %{older: nil, newer: nil} | |
| 420 | 417 | |
| 421 | − {{:before, _}, rows} -> | |
| 422 | − %{older: if(more?, do: List.last(rows).id), newer: hd(rows).id} | |
| 418 | + defp page_cursors({:after, _}, rows, more?), | |
| 419 | + do: %{older: List.last(rows).id, newer: if(more?, do: hd(rows).id)} | |
| 423 | 420 | |
| 424 | − {nil, rows} -> | |
| 425 | − %{older: if(more?, do: List.last(rows).id), newer: nil} | |
| 426 | − end | |
| 421 | + defp page_cursors({:before, _}, rows, more?), | |
| 422 | + do: %{older: if(more?, do: List.last(rows).id), newer: hd(rows).id} | |
| 427 | 423 | |
| 428 | − {rows, cursors} | |
| 429 | − end | |
| 424 | + defp page_cursors(nil, rows, more?), | |
| 425 | + do: %{older: if(more?, do: List.last(rows).id), newer: nil} | |
| 430 | 426 | |
| 431 | 427 | @doc """ |
| 432 | 428 | The names of `org`'s repositories that are gone but still have |
modified
lib/git_gud/git/cli.ex
+15
−20
| 134 | 134 | defp fold_name_only(out, dir, wanted) do |
| 135 | 135 | out |
| 136 | 136 | |> String.split(<<1>>, trim: true) |
| 137 | − |> Enum.reduce(%{}, fn block, acc -> | |
| 138 | − case String.split(block, "\n") do | |
| 139 | − [header | files] -> | |
| 140 | − case String.split(header, <<0>>, parts: 2) do | |
| 141 | − [sha_hex, iso] -> | |
| 142 | − touch = %{sha: hex_to_sha_or_nil(sha_hex), committed_at: parse_dt(iso)} | |
| 143 | − | |
| 144 | − files | |
| 145 | − |> Enum.flat_map(&entry_name(&1, dir)) | |
| 146 | − |> Enum.filter(&MapSet.member?(wanted, &1)) | |
| 147 | − |> Enum.reduce(acc, fn name, acc -> Map.put_new(acc, name, touch) end) | |
| 148 | − | |
| 149 | − _ -> | |
| 150 | − acc | |
| 151 | − end | |
| 152 | − | |
| 153 | − _ -> | |
| 154 | − acc | |
| 155 | − end | |
| 156 | − end) | |
| 137 | + |> Enum.reduce(%{}, &fold_block(&1, &2, dir, wanted)) | |
| 138 | + end | |
| 139 | + | |
| 140 | + defp fold_block(block, acc, dir, wanted) do | |
| 141 | + with [header | files] <- String.split(block, "\n"), | |
| 142 | + [sha_hex, iso] <- String.split(header, <<0>>, parts: 2) do | |
| 143 | + touch = %{sha: hex_to_sha_or_nil(sha_hex), committed_at: parse_dt(iso)} | |
| 144 | + | |
| 145 | + files | |
| 146 | + |> Enum.flat_map(&entry_name(&1, dir)) | |
| 147 | + |> Enum.filter(&MapSet.member?(wanted, &1)) | |
| 148 | + |> Enum.reduce(acc, fn name, acc -> Map.put_new(acc, name, touch) end) | |
| 149 | + else | |
| 150 | + _ -> acc | |
| 151 | + end | |
| 157 | 152 | end |
| 158 | 153 | |
| 159 | 154 | # `--name-only` reports full paths from the repo root; the listing |
modified
lib/git_gud/imports.ex
+12
−9
| 108 | 108 | Repo.transaction(fn -> |
| 109 | 109 | import_row = Repo.insert!(base) |
| 110 | 110 | |
| 111 | − items = | |
| 112 | − repos | |
| 113 | − |> Enum.uniq_by(& &1.full_name) | |
| 114 | − |> Enum.map(fn repo -> | |
| 115 | − case insert_item(import_row, repo) do | |
| 116 | − {:ok, item} -> item | |
| 117 | − {:error, changeset} -> Repo.rollback(changeset) | |
| 118 | − end | |
| 119 | − end) | |
| 111 | + items = insert_items!(import_row, repos) | |
| 120 | 112 | |
| 121 | 113 | import_row = |
| 122 | 114 | import_row |
| 143 | 135 | end |
| 144 | 136 | end |
| 145 | 137 | |
| 138 | + defp insert_items!(import_row, repos) do | |
| 139 | + repos | |
| 140 | + |> Enum.uniq_by(& &1.full_name) | |
| 141 | + |> Enum.map(fn repo -> | |
| 142 | + case insert_item(import_row, repo) do | |
| 143 | + {:ok, item} -> item | |
| 144 | + {:error, changeset} -> Repo.rollback(changeset) | |
| 145 | + end | |
| 146 | + end) | |
| 147 | + end | |
| 148 | + | |
| 146 | 149 | defp build_import(%User{} = actor, destination, attrs, token) do |
| 147 | 150 | attrs = stringify(attrs) |
| 148 | 151 | provider = attrs["provider"] |
modified
lib/git_gud/organizations.ex
+19
−16
| 61 | 61 | |> Organization.visibility_changeset(%{visibility: new_visibility}) |
| 62 | 62 | |> Repo.update() do |
| 63 | 63 | {:ok, updated} -> |
| 64 | − # Cascade: every repo with a lower visibility rank than the | |
| 65 | − # org bubbles up to match. | |
| 66 | − floor = floor_visibility(new_rank) | |
| 67 | − | |
| 68 | − if floor do | |
| 69 | − ranks = | |
| 70 | − for v <- Organization.valid_visibilities(), | |
| 71 | − Organization.visibility_rank(v) < new_rank, | |
| 72 | − do: v | |
| 73 | − | |
| 74 | − from(r in GitGud.Repositories.Repository, | |
| 75 | − where: r.organization_id == ^updated.id and r.visibility in ^ranks | |
| 76 | − ) | |
| 77 | − |> Repo.update_all(set: [visibility: floor, updated_at: DateTime.utc_now(:second)]) | |
| 78 | − end | |
| 79 | − | |
| 64 | + cascade_visibility(updated, new_rank) | |
| 80 | 65 | updated |
| 81 | 66 | |
| 82 | 67 | {:error, cs} -> |
| 85 | 70 | end) |
| 86 | 71 | end |
| 87 | 72 | |
| 73 | + # Cascade: every repo with a lower visibility rank than the org | |
| 74 | + # bubbles up to match. | |
| 75 | + defp cascade_visibility(%Organization{} = org, new_rank) do | |
| 76 | + floor = floor_visibility(new_rank) | |
| 77 | + | |
| 78 | + if floor do | |
| 79 | + ranks = | |
| 80 | + for v <- Organization.valid_visibilities(), | |
| 81 | + Organization.visibility_rank(v) < new_rank, | |
| 82 | + do: v | |
| 83 | + | |
| 84 | + from(r in GitGud.Repositories.Repository, | |
| 85 | + where: r.organization_id == ^org.id and r.visibility in ^ranks | |
| 86 | + ) | |
| 87 | + |> Repo.update_all(set: [visibility: floor, updated_at: DateTime.utc_now(:second)]) | |
| 88 | + end | |
| 89 | + end | |
| 90 | + | |
| 88 | 91 | defp floor_visibility(0), do: nil |
| 89 | 92 | defp floor_visibility(1), do: "internal" |
| 90 | 93 | defp floor_visibility(2), do: "private" |
modified
lib/git_gud/packages/formats/fdroid/arsc.ex
+14
−10
Click to load diff…
modified
lib/git_gud/pull_requests.ex
+18
−14
Click to load diff…
modified
lib/git_gud/repositories.ex
+24
−20
Click to load diff…
modified
lib/git_gud/repositories/hook_receiver.ex
+15
−11
Click to load diff…
modified
lib/git_gud/workflows.ex
+42
−39
Click to load diff…
modified
lib/git_gud_web/controllers/api/mobile/pull_request_controller.ex
+16
−12
Click to load diff…
modified
lib/git_gud_web/controllers/api/v1/workflow_controller.ex
+27
−18
Click to load diff…
modified
lib/git_gud_web/controllers/runner_controller.ex
+67
−70
Click to load diff…
modified
lib/git_gud_web/plugs/git_smart_http.ex
+17
−15
Click to load diff…
Parents: 4f41acc