Packages
fnord
0.9.30
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.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
key = {__MODULE__, :default_branch, root}
case :persistent_term.get(key, :miss) do
:miss ->
result = resolve_default_branch(root)
:persistent_term.put(key, result)
result
cached ->
cached
end
end
# Forks 2-4 git subprocesses (is_git_repo_at? + symbolic-ref, then
# rev-parse probes for main/master). Called per file under the index
# scan's async_stream via Source.hash and Source.exists?, so without
# the persistent_term wrapper above this is O(N) fork/exec round
# trips per pass - minutes of scan time on a thousand-file repo.
# Cached for the BEAM's lifetime: the branch tip doesn't advance
# during a single fnord invocation and the mode decision must stay
# stable across a scan.
defp resolve_default_branch(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