Packages
fnord
0.9.34
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/cmd/worktree_lifecycle.ex
defmodule Cmd.WorktreeLifecycle do
@moduledoc """
Shared worktree lifecycle helpers used by `Cmd.Worktrees`, `Cmd.Conversations`,
and `Cmd.Ask` for managing worktree cleanup, deletion prompts, and conversation
metadata updates.
"""
@spec warn_if_unmerged(String.t(), map()) :: :ok | {:error, :cancelled}
@doc """
Warns the user if the worktree branch has unmerged commits relative to its
base branch. Returns `:ok` to continue or `{:error, :cancelled}` if the
user declines to proceed.
"""
def warn_if_unmerged(root, %{branch: branch, base_branch: base_branch})
when is_binary(branch) and is_binary(base_branch) do
case GitCli.Worktree.diff_against_base(root, branch, base_branch) do
{:ok, diff} when byte_size(diff) > 0 ->
UI.warn("Branch #{branch} has commits that have NOT been merged into #{base_branch}.")
if UI.confirm("Proceed with deletion anyway?") do
:ok
else
{:error, :cancelled}
end
_ ->
:ok
end
end
def warn_if_unmerged(_root, _meta), do: :ok
@spec delete_worktree(String.t(), String.t()) :: {:ok, :ok} | {:error, atom()}
@doc """
Attempts a clean worktree removal. If the worktree has uncommitted changes,
warns the user and asks for confirmation before force-deleting. This helper
only removes the worktree itself; callers should only clean up the branch
after worktree removal succeeds.
"""
def delete_worktree(root, path) do
case GitCli.Worktree.delete(root, path) do
{:ok, :ok} ->
{:ok, :ok}
{:error, _} when is_binary(path) ->
if GitCli.Worktree.has_uncommitted_changes?(path) do
UI.warn("Worktree at #{path} has uncommitted changes that will be lost.")
if UI.confirm("Force delete anyway?") do
GitCli.Worktree.force_delete(root, path)
else
{:error, :cancelled}
end
else
{:error, :git_failed}
end
end
end
@spec clear_worktree_from_conversation(String.t()) :: :ok
@doc """
Removes worktree metadata from a conversation. On the next session,
AI.Agent.Coordinator.worktree_context_msg/1 inspects live metadata during
bootstrap and injects a fresh "no worktree, create one" system message, so
no trailing message needs to be appended here. Mutating data.messages also
breaks `fnord replay`, which assumes the last message is the assistant's
final reply.
"""
def clear_worktree_from_conversation(conv_id) do
conv = Store.Project.Conversation.new(conv_id)
with {:ok, data} <- Store.Project.Conversation.read(conv) do
updated_metadata =
data.metadata
|> Map.delete(:worktree)
|> Map.delete("worktree")
|> Map.delete(:gitignored_writes)
|> Map.delete("gitignored_writes")
Store.Project.Conversation.write(conv, %{data | metadata: updated_metadata})
end
:ok
end
@spec cleanup_worktree_for_conversation(Store.Project.Conversation.t()) :: :ok
@doc """
Checks whether a conversation has an associated worktree on disk and offers
to delete it. Used during conversation deletion to avoid orphaned worktrees.
When the user confirms cleanup, the local branch is only deleted after the
worktree itself is removed successfully.
"""
def cleanup_worktree_for_conversation(conversation) do
with {:ok, data} <- safe_read_conversation(conversation),
meta <- extract_worktree_meta(data.metadata),
true <- is_map(meta) and is_binary(Map.get(meta, :path)),
true <- File.dir?(meta.path),
{:ok, root} <- GitCli.Worktree.project_root() do
UI.info("Conversation has worktree", meta.path)
status =
cond do
GitCli.Worktree.has_uncommitted_changes?(meta.path) -> :dirty
has_unmerged_commits?(root, meta) -> :unmerged
true -> :clean
end
case status do
:dirty ->
UI.warn("Worktree has uncommitted changes.")
prompt_worktree_deletion(root, meta)
:unmerged ->
UI.warn("Worktree branch has unmerged commits.")
prompt_worktree_deletion(root, meta)
:clean ->
prompt_worktree_deletion(root, meta)
end
else
_ -> :ok
end
end
defp prompt_worktree_deletion(root, meta) do
if UI.confirm("Delete worktree and local branch?") do
case GitCli.Worktree.force_delete(root, meta.path) do
{:ok, :ok} ->
UI.info("Deleted worktree", meta.path)
if is_binary(meta.branch) do
case GitCli.Worktree.delete_branch(root, meta.branch) do
{:ok, :ok} -> UI.info("Deleted branch", meta.branch)
{:error, reason} -> UI.warn("Failed to delete branch: #{reason}")
end
end
{:error, reason} ->
UI.warn("Failed to delete worktree: #{reason}")
end
end
:ok
end
defp has_unmerged_commits?(root, %{branch: branch, base_branch: base_branch})
when is_binary(branch) and is_binary(base_branch) do
case GitCli.Worktree.diff_against_base(root, branch, base_branch) do
{:ok, diff} when byte_size(diff) > 0 -> true
_ -> false
end
end
defp has_unmerged_commits?(_root, _meta), do: false
# Reads conversation data, suppressing errors from corrupt or minimal files
# that may call UI functions not available in all contexts.
defp safe_read_conversation(conversation) do
Store.Project.Conversation.read(conversation)
rescue
_ -> {:error, :unreadable}
end
defp extract_worktree_meta(metadata) when is_map(metadata) do
metadata
|> GitCli.Worktree.normalize_worktree_meta_in_parent()
|> Map.get(:worktree)
end
end