Module: Esp::Vcs

Defined in:
lib/esp/vcs.rb

Overview

A thin wrapper over the ‘git` CLI, scoped to a working-tree `root`. Backs the diff-review loop (step 20): list working-tree changes, show one file’s diff, stage approved files, discard rejected ones. We shell out rather than bind libgit2 (nothing extra to ship) — the project is already a git repo.

Every method takes an explicit ‘root` so it operates on the *user’s* mod project, never the toolchain repo, and so it’s testable against a scratch repo. Git failures raise GitError; the Operations layer maps that to a caller-facing error.

Defined Under Namespace

Classes: Change, GitError

Class Method Summary collapse

Class Method Details

.changes(root:, scope: nil) ⇒ Object

Working-tree changes under ‘scope` (a path prefix, e.g. “mods”), each a Change with status ∈ added | modified | deleted | renamed and a staged flag. Untracked files show up as `added`.



22
23
24
25
# File 'lib/esp/vcs.rb', line 22

def changes(root:, scope: nil)
  scope_args = scope.nil? || scope.empty? ? [] : ['--', scope]
  parse_status(capture(root, 'status', '--porcelain=v1', '-z', *scope_args))
end

.discard(root:, path:) ⇒ Object

Discard a rejected change: restore a tracked file to HEAD, delete an agent-created untracked file. Destructive — callers confirm first.



50
51
52
53
54
55
56
# File 'lib/esp/vcs.rb', line 50

def discard(root:, path:)
  if tracked?(root, path)
    run(root, 'checkout', 'HEAD', '--', path)
  else
    File.delete(File.join(root, path))
  end
end

.file_diff(root:, path:) ⇒ Object

Unified diff of one file against HEAD. An untracked (agent-created) file has no HEAD entry, so we diff it against the null device to render it as an all-add patch.



30
31
32
33
34
35
36
37
38
# File 'lib/esp/vcs.rb', line 30

def file_diff(root:, path:)
  if tracked?(root, path)
    capture(root, 'diff', 'HEAD', '--', path)
  else
    # --no-index exits 1 whenever the files differ; that's expected here,
    # not a failure.
    capture(root, 'diff', '--no-index', '--', File::NULL, path, allow_fail: true)
  end
end

.run_git_init(root) ⇒ Object

Initialise a git repo at ‘root`. Used by the new-project flow (step 23 slice 5) so the freshly-scaffolded project tree is a tracked working tree from minute one.



65
66
67
68
# File 'lib/esp/vcs.rb', line 65

def run_git_init(root)
  FileUtils.mkdir_p(root)
  run(root, 'init', '-q')
end

.stage(root:, paths:) ⇒ Object

Stage approved paths (‘git add`). The change stays in the working tree; the human commits when ready.



42
43
44
45
46
# File 'lib/esp/vcs.rb', line 42

def stage(root:, paths:)
  return if paths.empty?

  run(root, 'add', '--', *paths)
end

.tracked?(root, path) ⇒ Boolean

Returns:

  • (Boolean)


58
59
60
# File 'lib/esp/vcs.rb', line 58

def tracked?(root, path)
  !capture(root, 'ls-files', '--', path).strip.empty?
end