Packages

fnord

0.9.23

AI code archaeology

Current section

Files

Jump to
fnord lib git_cli.ex
Raw

lib/git_cli.ex

# lib/git_cli.ex
defmodule GitCli do
@moduledoc """
Wrapper for direct git CLI calls.
Covers repo classification (`is_git_repo?/0`, `worktree_root/0`),
branch reporting (`current_branch/0`, `default_branch/1`), tree
enumeration for indexing (`ls_tree/2`, `show_blob/3`), gitignore
resolution (`ignored_files/1`), and formatted user-facing messages
(`git_info/0`).
Note: `default_branch/1` resolves the project's *indexing* branch
with a strict fallback chain (origin/HEAD → main → master → nil).
For the looser worktree-root resolution that falls back to the
current branch, see `GitCli.Worktree.default_base_branch/1`.
"""
@spec is_git_repo?() :: boolean()
def is_git_repo? do
case System.cmd("git", ["rev-parse", "--is-inside-work-tree"],
cd: effective_git_dir(),
stderr_to_stdout: true
) do
{"true\n", 0} -> true
_ -> false
end
end
@spec is_git_repo_at?(String.t() | nil) :: boolean()
def is_git_repo_at?(nil), do: false
def is_git_repo_at?(path) when is_binary(path) do
case System.cmd("git", ["rev-parse", "--is-inside-work-tree"],
cd: path,
stderr_to_stdout: true
) do
{"true\n", 0} -> true
_ -> false
end
end
def repo_root() do
git = System.find_executable("git")
if git do
case System.cmd(git, ["rev-parse", "--show-toplevel"],
cd: effective_git_dir(),
stderr_to_stdout: true
) do
{out, 0} -> String.trim(out)
_ -> nil
end
else
nil
end
end
# We currently treat any effective directory inside a git working tree as
# worktree-aware enough for callers using this predicate, including detached
# HEAD state.
def is_worktree? do
is_git_repo?()
end
def worktree_root() do
if is_worktree?() do
case System.cmd("git", ["rev-parse", "--show-toplevel"],
cd: effective_git_dir(),
stderr_to_stdout: true
) do
{result, 0} -> String.trim(result)
_ -> nil
end
else
nil
end
end
# Branch reporting follows the same effective directory semantics as the
# other repo and worktree helpers in this module.
@spec current_branch() :: String.t() | nil
def current_branch() do
git = System.find_executable("git")
if git do
case System.cmd(git, ["rev-parse", "--abbrev-ref", "HEAD"],
cd: effective_git_dir(),
stderr_to_stdout: true
) do
{out, 0} ->
case String.trim(out) do
"HEAD" ->
case System.cmd(git, ["rev-parse", "--short", "HEAD"],
cd: effective_git_dir(),
stderr_to_stdout: true
) do
{sha, 0} -> "@" <> String.trim(sha)
_ -> nil
end
branch ->
branch
end
_ ->
nil
end
else
nil
end
end
@spec branch_name(String.t(), String.t()) :: String.t() | nil
defp branch_name(git, dir) do
case System.cmd(git, ["rev-parse", "--abbrev-ref", "HEAD"],
cd: dir,
stderr_to_stdout: true
) do
{out, 0} ->
case String.trim(out) do
"HEAD" ->
case System.cmd(git, ["rev-parse", "--short", "HEAD"],
cd: dir,
stderr_to_stdout: true
) do
{sha, 0} -> "@" <> String.trim(sha)
_ -> nil
end
branch ->
branch
end
_ ->
nil
end
end
@spec git_info() :: String.t()
def git_info() do
git = System.find_executable("git")
if git do
root =
case System.cmd(git, ["rev-parse", "--show-toplevel"],
cd: effective_git_dir(),
stderr_to_stdout: true
) do
{out, 0} -> String.trim(out)
_ -> nil
end
branch =
if root do
branch_name(git, effective_git_dir())
end
if root && branch do
"""
You are working in a git repository.
The current branch is `#{branch}`.
The git root is `#{root}`.
"""
else
"Note: this project is not under git version control."
end
else
"Note: git executable not found on PATH."
end
end
@spec effective_git_dir() :: String.t()
defp effective_git_dir() do
Settings.get_project_root_override() || File.cwd!()
end
@spec ignored_files(String.t() | nil) :: map()
@doc """
Returns an empty map if root is nil, otherwise behaves as before.
"""
def ignored_files(nil), do: %{}
def ignored_files(root) when is_binary(root) do
case System.cmd("git", ["ls-files", "--others", "--ignored", "--exclude-standard"],
cd: root,
stderr_to_stdout: true
) do
{out, 0} ->
out
|> String.split("\n", trim: true)
|> Enum.map(&Path.absname(&1, root))
|> Map.new(&{&1, true})
_ ->
%{}
end
end
@doc """
Returns the repository's default branch for indexing purposes:
1. `origin/HEAD` - the remote's declared default (usually main).
2. Local `main` or `master`, in that order.
3. `nil` - do not silently fall back to the current branch, since
that would make `fnord index` on a feature branch index the
feature branch rather than the project's canonical source.
Callers fall back to filesystem-mode indexing when this returns nil,
so the user still gets their working tree indexed; they just won't
get default-branch semantics.
"""
@spec default_branch(String.t() | nil) :: String.t() | nil
def default_branch(nil), do: nil
def default_branch(root) when is_binary(root) do
cond do
not is_git_repo_at?(root) -> nil
branch = remote_head(root) -> branch
branch_exists?(root, "main") -> "main"
branch_exists?(root, "master") -> "master"
true -> nil
end
end
defp remote_head(root) do
case System.cmd("git", ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"],
cd: root,
stderr_to_stdout: true
) do
{out, 0} ->
case out |> String.trim() |> String.replace_prefix("refs/remotes/origin/", "") do
"" -> nil
branch -> branch
end
_ ->
nil
end
end
defp branch_exists?(root, branch) do
case System.cmd("git", ["rev-parse", "--verify", "--quiet", "refs/heads/#{branch}"],
cd: root,
stderr_to_stdout: true
) do
{_, 0} -> true
_ -> false
end
end
@doc """
Lists every blob in `branch`'s tree as `{blob_sha, rel_path}` pairs.
The blob sha is git's content-addressed hash and is stable across
clones and checkouts, so it's usable as a freshness key for the
indexer.
"""
@spec ls_tree(String.t(), String.t()) :: {:ok, [{String.t(), String.t()}]} | {:error, term}
def ls_tree(root, branch) when is_binary(root) and is_binary(branch) do
case System.cmd(
"git",
["ls-tree", "-r", "--full-tree", "--format=%(objectname)\t%(path)", branch],
cd: root,
stderr_to_stdout: true
) do
{out, 0} ->
entries =
out
|> String.split("\n", trim: true)
|> Enum.flat_map(fn line ->
case String.split(line, "\t", parts: 2) do
[sha, path] when byte_size(sha) > 0 and byte_size(path) > 0 -> [{sha, path}]
_ -> []
end
end)
{:ok, entries}
{err, _} ->
{:error, String.trim(err)}
end
end
@doc """
Returns the content of `rel_path` as it exists on `branch`, or an
error tuple if git rejects the request (missing file, invalid branch,
etc.). Content is returned as a binary; binaries that aren't valid
UTF-8 are still returned — callers decide how to handle them.
"""
@spec show_blob(String.t(), String.t(), String.t()) :: {:ok, binary} | {:error, term}
def show_blob(root, branch, rel_path)
when is_binary(root) and is_binary(branch) and is_binary(rel_path) do
case System.cmd("git", ["show", "#{branch}:#{rel_path}"], cd: root, stderr_to_stdout: false) do
{out, 0} -> {:ok, out}
{err, _} -> {:error, String.trim(err)}
end
end
end