Packages
fnord
0.9.38
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/git_cli/worktree/review/default.ex
defmodule GitCli.Worktree.Review.Default do
@moduledoc """
Real implementation of the `GitCli.Worktree.Review` behaviour. See
`GitCli.Worktree.Review` for the public contract and per-function
documentation.
Worktree operations go through the `GitCli.Worktree` facade so a test
double on the `:git_worktree` Globals key intercepts them.
"""
@behaviour GitCli.Worktree.Review
alias GitCli.Worktree.Review
@impl Review
def interactive_review(root, %{path: path, branch: branch, base_branch: base_branch}, opts) do
unless UI.is_tty?() do
throw(:skip)
end
print_header()
log_ignored_files(opts)
target = GitCli.Worktree.current_branch(root) || "HEAD"
unless UI.confirm(wt_prompt("Inspect changes from worktree branch #{branch}?")) do
throw(:skip)
end
show_diff(root, branch, base_branch)
unless UI.confirm(wt_prompt("Merge #{branch} into #{target}?")) do
throw(:skip)
end
# Pre-merge validation: run in the worktree
case run_validation(path, "Pre-merge") do
:ok ->
:ok
{:failed, summary} ->
UI.error("Pre-merge validation failed")
UI.say(summary)
unless UI.confirm(wt_prompt("Merge anyway despite validation failure?")) do
throw({:validation_failed, :pre_merge, summary})
end
end
case do_merge_with_post_validation(root, path, branch, target) do
{:ok, sha} ->
if maybe_cleanup(root, path, branch) do
throw({:cleaned_up, sha, :interactive})
end
:ok
{:validation_failed, :post_merge, _summary} = failure ->
throw(failure)
{:merge_failed, _reason} = failure ->
throw(failure)
end
catch
:throw, :skip -> :ok
:throw, {:cleaned_up, sha, mode} -> {:cleaned_up, sha, mode}
:throw, {:validation_failed, _, _} = failure -> failure
:throw, {:merge_failed, _reason} = failure -> failure
end
@impl Review
def auto_merge(root, %{path: path, branch: branch, base_branch: base_branch}, opts) do
print_header()
log_ignored_files(opts)
target = GitCli.Worktree.current_branch(root) || "HEAD"
UI.info("Auto-merge", "#{branch} into #{target}")
show_diff(root, branch, base_branch)
# Pre-merge validation: block on failure
case run_validation(path, "Pre-merge") do
:ok -> :ok
{:failed, summary} -> throw({:validation_failed, :pre_merge, summary})
end
case do_merge_with_post_validation(root, path, branch, target) do
{:ok, sha} ->
cleanup(root, path, branch)
{:cleaned_up, sha, :auto}
{:validation_failed, :post_merge, _summary} = failure ->
throw(failure)
{:merge_failed, _reason} = failure ->
throw(failure)
end
catch
:throw, {:validation_failed, _, _} = failure -> failure
:throw, {:merge_failed, _reason} = failure -> failure
end
# Merges the worktree branch into root, then runs post-merge validation.
# If the merge command fails, returns the failure to the caller. If
# post-merge validation fails, resets HEAD to the pre-merge state. A hard
# reset is necessary because ff merges can advance HEAD by multiple commits,
# and `git revert HEAD` would only undo the last one.
defp do_merge_with_post_validation(root, path, branch, target) do
pre_merge_sha = GitCli.Worktree.head_sha_full(root)
case GitCli.Worktree.merge(root, path) do
{:ok, _} ->
post_merge_sha = GitCli.Worktree.head_sha_full(root)
UI.info("Merged", "#{branch} into #{target}")
case run_validation(root, "Post-merge") do
:ok ->
{:ok, {pre_merge_sha, post_merge_sha}}
{:failed, summary} ->
UI.error("Post-merge validation failed; reverting merge")
case GitCli.Worktree.reset_hard(root, pre_merge_sha) do
{:ok, :ok} -> UI.info("Reset to pre-merge state")
{:error, reason} -> UI.warn("Reset failed: #{reason}")
end
{:validation_failed, :post_merge, summary}
end
{:error, reason} ->
UI.error("Merge failed: #{reason}")
{:merge_failed, reason}
end
end
# Runs validation rules against the given root and returns :ok or {:failed, summary}.
# Returns :ok when there are no rules, no matches, or no changes.
defp run_validation(root, label) do
with {:ok, project} <- Store.get_project() do
UI.info(label, "running validation in #{root}")
result = Validation.Rules.run(project.name, root)
case result do
{:ok, :no_changes} ->
:ok
{:ok, :no_rules, _, _} ->
:ok
{:ok, :no_matches, _, _} ->
:ok
{:ok, _results, _fingerprint} ->
UI.info(label, "validation passed")
:ok
{:error, :discovery_failed} ->
UI.warn("#{label}: could not determine changed files")
:ok
{:error, _, _} ->
{:failed, Validation.Rules.summarize(result)}
end
else
_ -> :ok
end
end
defp log_ignored_files(opts) do
case Keyword.get(opts, :ignored_files, []) do
[] -> :ok
files -> Enum.each(files, &UI.info("Copied ignored file", &1))
end
end
defp print_header do
header =
IO.ANSI.format(
[:cyan_background, :black, :bright, " ◆ Worktree Review ◆ ", :reset],
true
)
separator =
IO.ANSI.format(
[:cyan, String.duplicate("─", 60), :reset],
true
)
IO.puts(:stderr, "\n#{separator}\n#{header}\n#{separator}\n")
UI.Tee.write(["\n", separator, "\n", header, "\n", separator, "\n\n"])
end
# Formats a worktree review prompt with color so it stands out from
# the surrounding log output.
defp wt_prompt(msg) do
IO.ANSI.format([:bright, :cyan, "◆ ", :reset, :bright, msg, :reset], true)
|> IO.chardata_to_string()
end
defp show_diff(root, branch, base_branch) do
case GitCli.Worktree.diff_against_base(root, branch, base_branch) do
{:ok, diff} when byte_size(diff) > 0 ->
diff
|> Review.colorize_diff()
|> UI.puts()
{:ok, _} ->
UI.info("Diff", "No changes between #{base_branch} and #{branch}")
{:error, reason} ->
UI.warn("Could not generate diff: #{reason}")
end
end
# Returns true if the worktree was deleted.
defp maybe_cleanup(root, path, branch) do
if UI.confirm(wt_prompt("Delete worktree and local branch #{branch}?")) do
cleanup(root, path, branch)
true
else
false
end
end
defp cleanup(root, path, branch) do
case GitCli.Worktree.force_delete(root, path) do
{:ok, _} -> UI.info("Deleted worktree", path)
{:error, reason} -> UI.warn("Failed to delete worktree: #{reason}")
end
case GitCli.Worktree.delete_branch(root, branch) do
{:ok, _} ->
UI.info("Deleted branch", branch)
{:error, _} ->
case GitCli.Worktree.force_delete_branch(root, branch) do
{:ok, _} -> UI.info("Force-deleted branch", branch)
{:error, reason} -> UI.warn("Failed to delete branch: #{reason}")
end
end
end
@impl Review
def colorize_diff(diff) do
diff
|> String.split("\n")
|> Enum.map(fn line ->
cond do
String.starts_with?(line, "+") -> Owl.Data.tag(line <> "\n", [:white, :green_background])
String.starts_with?(line, "-") -> Owl.Data.tag(line <> "\n", [:white, :red_background])
true -> line <> "\n"
end
end)
end
end