Packages
fnord
0.9.26
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.ex
defmodule GitCli.Worktree.Review do
@moduledoc """
Shared interactive flow for reviewing, merging, and cleaning up a
fnord-managed worktree. Used by both `Cmd.Ask` (post-completion) and
`Cmd.Worktrees merge`.
Includes pre-merge and post-merge validation gates that run the project's
configured validation rules against the worktree (before merge) and the
main checkout (after merge). Post-merge validation failure triggers an
automatic revert, while merge command failures are returned to the caller.
"""
@type worktree_info :: %{
path: String.t(),
branch: String.t(),
base_branch: String.t()
}
@type merge_range :: {String.t(), String.t()} | nil
@type review_result ::
:ok
| {:cleaned_up, merge_range(), :interactive | :auto}
| {:validation_failed, :pre_merge | :post_merge, String.t()}
| {:merge_failed, String.t()}
@spec interactive_review(String.t(), worktree_info(), keyword()) :: review_result()
@doc """
Walks the user through inspecting the diff, merging, and optionally deleting
the worktree and its local branch. Runs validation before and after merge.
"""
def interactive_review(root, meta, opts \\ [])
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
@spec auto_merge(String.t(), worktree_info(), keyword()) :: review_result()
@doc """
Merges worktree changes and cleans up without prompting. Runs validation
before and after merge. Pre-merge validation failure blocks the merge.
"""
def auto_merge(root, meta, opts \\ [])
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
|> 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
@doc "Colorizes a unified diff string for terminal display."
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