Module: Moult::Git

Defined in:
lib/moult/git.rb

Overview

Thin, injection-safe wrapper over the git CLI. All commands run with an explicit working directory and argument array (never a shell string), and failures surface as nil/false rather than exceptions so callers can degrade gracefully outside a repository.

Class Method Summary collapse

Class Method Details

.capture(dir, *args) ⇒ Object

Run a git subcommand, returning stdout, or nil on any failure.



76
77
78
79
80
81
# File 'lib/moult/git.rb', line 76

def capture(dir, *args)
  out, _, status = Open3.capture3("git", *args, chdir: dir)
  status.success? ? out : nil
rescue SystemCallError
  nil
end

.diff_name_status(dir, ref) ⇒ String?

git diff --name-status REF: one "\t" line per changed file between ref and the working tree (renames carry two paths). Empty string when nothing changed; nil on failure.

Returns:

  • (String, nil)


62
63
64
# File 'lib/moult/git.rb', line 62

def diff_name_status(dir, ref)
  capture(dir, "diff", "--name-status", ref)
end

.diff_unified_zero(dir, ref) ⇒ String?

git diff --unified=0 REF: a context-free unified diff between ref and the working tree. Zero context means each hunk header's new-side range (@@ -a,b +c,d @@) is exactly the changed/added lines — what the gate needs to scope findings to the diff. Empty string when nothing changed; nil on failure.

Returns:

  • (String, nil)


71
72
73
# File 'lib/moult/git.rb', line 71

def diff_unified_zero(dir, ref)
  capture(dir, "diff", "--unified=0", ref)
end

.head_ref(dir) ⇒ String?

Returns the HEAD commit sha, or nil outside a repo.

Returns:

  • (String, nil)

    the HEAD commit sha, or nil outside a repo



22
23
24
25
# File 'lib/moult/git.rb', line 22

def head_ref(dir)
  out = capture(dir, "rev-parse", "HEAD")
  out&.strip
end

.listed_files(dir) ⇒ Array<String>

Returns tracked + untracked-but-not-ignored files, relative to dir, respecting .gitignore. Empty outside a repo.

Returns:

  • (Array<String>)

    tracked + untracked-but-not-ignored files, relative to dir, respecting .gitignore. Empty outside a repo.



29
30
31
32
33
34
# File 'lib/moult/git.rb', line 29

def listed_files(dir)
  out = capture(dir, "ls-files", "--cached", "--others", "--exclude-standard", "-z")
  return [] unless out

  out.split("\x0").reject(&:empty?)
end

.log_name_only(dir, since:) ⇒ String?

Raw git log file listing for churn: one path per line, blank lines between commits. Each commit lists a touched path at most once.

Returns:

  • (String, nil)

    nil outside a repo



39
40
41
# File 'lib/moult/git.rb', line 39

def log_name_only(dir, since:)
  capture(dir, "log", "--since=#{since}", "--name-only", "--pretty=format:")
end

.merge_base(dir, base_ref) ⇒ String?

The common ancestor of base_ref and HEAD — the "new code" boundary, the same merge-base semantics SonarQube/CodeScene use to scope a diff.

Returns:

  • (String, nil)

    the merge-base sha, or nil if it can't be resolved (base_ref unknown, shallow clone with no common history, outside a repo)



53
54
55
56
# File 'lib/moult/git.rb', line 53

def merge_base(dir, base_ref)
  out = capture(dir, "merge-base", base_ref, "HEAD")
  out&.strip
end

.repo?(dir) ⇒ Boolean

Returns whether dir is inside a git work tree.

Returns:

  • (Boolean)

    whether dir is inside a git work tree



14
15
16
17
18
19
# File 'lib/moult/git.rb', line 14

def repo?(dir)
  out, _, status = Open3.capture3("git", "rev-parse", "--is-inside-work-tree", chdir: dir)
  status.success? && out.strip == "true"
rescue SystemCallError
  false
end