Class: RSpecTracer::RemoteCache::GitAncestry Private

Inherits:
Object
  • Object
show all
Defined in:
lib/rspec_tracer/remote_cache/git_ancestry.rb

Overview

This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.

Git ancestry walker. Given the current branch + default branch, answers “which commit should I upload cache under?” and “which prior commits should I try as cache candidates?”

Behavior preserved verbatim from 1.x ‘RemoteCache::Repo` - USER_FACING_SURFACE.md pins the 25-commit ancestry, branch_refs, history-rewrite resilience, merge-commit handling, and shallow- clone guidance as user contracts. Any change here affects every existing CI config that sets `fetch-depth: 25` or relies on the current “nearest ancestor” heuristic.

Four scenarios this class handles:

1. Main branch build (GIT_BRANCH == GIT_DEFAULT_BRANCH):
   merge_base_branch! is a no-op. branch_ref = HEAD (or HEAD^1
   if HEAD is itself a merge commit, e.g. main just absorbed a
   feature branch via --no-ff merge). Ancestry walks HEAD^'s
   linear history up to 25 commits.

2. GitHub Actions PR (checkout of refs/pull/N/merge, detached
   HEAD on a synthetic merge commit):
   merge_base_branch! runs `git fetch origin <branch>:<branch>`
   + `git checkout <branch>` + `git merge origin/<default>
   --no-edit --no-ff`. After this HEAD is a (possibly new) merge
   commit. branch_ref = HEAD^1 (the PR branch tip). Ancestry
   walks HEAD^1's 25-commit history UNION `HEAD^1..origin/HEAD`
   (default branch commits the PR hasn't absorbed yet).

3. Jenkins / CircleCI / Travis PR (checkout of raw PR branch):
   merge_base_branch! materializes the merge commit that GHA
   provides automatically. Result is identical to scenario 2.

4. PR branch behind main: same flow as 2/3. The explicit merge
   ensures RSpec runs against the merged state and ancestry
   picks up main's recent commits as candidates.

The merge is a WORKING-TREE MUTATION. Callers should run ‘merge_base_branch!` exactly once at the start of each Rake task invocation (download + upload each instantiate a fresh orchestrator and re-run the merge; idempotent on a clean tree). See RSPEC_TRACER.md “Caching on CI” for the user-facing rationale.

Defined Under Namespace

Classes: GitAncestryError

Constant Summary collapse

ANCESTRY_DEPTH =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Matches 1.x. 25 commits is a 14-year-stable tradeoff between cache hit rate on slow trunks and ancestry-walk cost.

25

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(default_branch:, branch:) ⇒ GitAncestry

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal method on the tracer pipeline.

Raises:



66
67
68
69
70
71
72
# File 'lib/rspec_tracer/remote_cache/git_ancestry.rb', line 66

def initialize(default_branch:, branch:)
  raise GitAncestryError, 'default_branch is required' if default_branch.nil? || default_branch.to_s.empty?
  raise GitAncestryError, 'branch is required' if branch.nil? || branch.to_s.empty?

  @default_branch_name = default_branch.to_s.chomp
  @branch_name = branch.to_s.chomp
end

Instance Attribute Details

#branch_nameObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal attribute.



62
63
64
# File 'lib/rspec_tracer/remote_cache/git_ancestry.rb', line 62

def branch_name
  @branch_name
end

#default_branch_nameObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal attribute.



62
63
64
# File 'lib/rspec_tracer/remote_cache/git_ancestry.rb', line 62

def default_branch_name
  @default_branch_name
end

Instance Method Details

#ancestry_refsObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Hash=> committer_timestamp of candidate ancestor commits. Newest-first ordering is applied by the orchestrator when merging with branch_refs; this method returns insertion order.

Walk rule:

refs = Set[]
if merge_commit?: refs |= rev-list --max-count=25 branch_ref..origin/HEAD
refs |= rev-list --max-count=25 branch_ref
refs -= ignored_refs  # drop synthetic HEAD when merged

Returns {} when the walk yields nothing (new repo, shallow clone shorter than 25, etc.). Not an error.



134
135
136
137
138
139
140
141
142
143
144
# File 'lib/rspec_tracer/remote_cache/git_ancestry.rb', line 134

def ancestry_refs
  return @ancestry_refs if defined?(@ancestry_refs)

  branch_ref # materialize ignored_refs

  ref_list = Set.new
  ref_list |= rev_list("#{@branch_ref}..origin/HEAD") if merge_commit?
  ref_list |= rev_list(@branch_ref.to_s)

  @ancestry_refs = refs_committer_timestamp(ref_list - @ignored_refs)
end

#branch_refObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The SHA to upload cache under, and to seed ancestry walk from. When HEAD is a merge commit (normal PR case after merge_base_branch! runs), branch_ref = HEAD^1 (the real branch tip). Uploading under HEAD directly would key the cache under a synthetic merge SHA that no future build’s ancestry walk can reach.



107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/rspec_tracer/remote_cache/git_ancestry.rb', line 107

def branch_ref
  return @branch_ref if defined?(@branch_ref)

  head = head_ref
  if merge_commit?
    parents = merged_parents
    @branch_ref = parents.first
    @ignored_refs = [head]
  else
    @branch_ref = head
    @ignored_refs = []
  end
  @branch_ref
end

#merge_base_branch!Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Materialize the PR-merged state: fetch + checkout + merge default. No-op on main builds. Idempotent on a clean tree (a subsequent ‘git merge origin/<default>` when already merged produces “Already up to date”, no new merge commit).

Raises GitAncestryError on fetch/checkout/merge failure. The orchestrator catches and logs; the user’s Rake task exits cleanly with a warning but proceeds to cold-run.



94
95
96
97
98
99
100
# File 'lib/rspec_tracer/remote_cache/git_ancestry.rb', line 94

def merge_base_branch!
  return if @default_branch_name == @branch_name

  pull_remote_branch! if current_branch != @branch_name
  merge_default_branch!
  reset_memo!
end

#merge_commit?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

True when HEAD has two parents (i.e., a merge commit). Cached.

Returns:

  • (Boolean)


80
81
82
83
84
# File 'lib/rspec_tracer/remote_cache/git_ancestry.rb', line 80

def merge_commit?
  return @merge_commit if defined?(@merge_commit)

  @merge_commit = system('git', 'rev-parse', 'HEAD^2', out: File::NULL, err: File::NULL)
end

#pr_build?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

True when this is a PR build (current branch != default branch).

Returns:

  • (Boolean)


75
76
77
# File 'lib/rspec_tracer/remote_cache/git_ancestry.rb', line 75

def pr_build?
  @branch_name != @default_branch_name
end