How actions/checkout Handles Ref Movement During Fetch

When fetching full history, actions/checkout performs a two-step verification where it fetches the initial ref, detects if the branch moved via force-push, and triggers a second targeted fetch using the exact commit SHA to ensure the correct revision is checked out.

The actions/checkout GitHub Action must guarantee that the checked out commit matches the requested reference even when that reference changes between fetch operations. When fetch-depth is set to 0 (full history), the action implements a defensive ref-movement detection mechanism in src/git-source-provider.ts that handles force-pushes and rapid branch updates during the fetch process.

The Two-Step Fetch Strategy

When full history is requested via fetch-depth: 0, the action cannot rely on a single atomic fetch operation. Instead, it implements a verification cycle that ensures the final checkout matches the exact commit the workflow requested at execution time.

Initial Fetch with Generic Refspec

The action begins by fetching the requested reference using a standard refspec generated from the user-supplied ref input. This operation pulls down the objects required for checkout but leaves a window where the remote reference could move.

In src/git-source-provider.ts, this initial fetch occurs when settings.fetchDepth <= 0 triggers the full-history fetch path. The implementation calls GitCommandManager.fetch() located in src/git-command-manager.ts to execute the underlying git fetch command.

Detecting Ref Movement

After the initial fetch completes, the action verifies whether the reference still points to the expected commit. The source code checks if the local reference matches the remote SHA. If a force-push or rapid update occurred between the fetch initiation and completion, these values diverge.

The relevant logic includes a comment explaining the intent:

"When all history is fetched, the ref we're interested in may have moved to a different commit (push or force push). If so, fetch again with a targeted refspec."

This detection mechanism specifically targets branch references. Tags are fetched by their exact refspec and do not require secondary verification or the ref-movement check.

Targeted Re-fetch with Exact SHA

When the action detects ref movement, it constructs a targeted refspec that explicitly requests the exact commit SHA rather than the symbolic branch name. This secondary fetch ensures the new tip is available locally regardless of how the branch moved.

The targeted refspec format follows the pattern:

`${remoteSha}:refs/heads/${settings.ref}`

This approach guarantees that even if the branch receives another push during the second fetch, the action retrieves the specific commit SHA it originally resolved.

Implementation Details in git-source-provider.ts

The core logic resides in src/git-source-provider.ts, where the action orchestrates the fetch sequence. The implementation distinguishes between shallow clones and full-history fetches, applying the ref-movement check only when settings.fetchDepth <= 0.

The GitCommandManager.fetch() method in src/git-command-manager.ts handles the low-level Git command construction, accepting parameters for depth, refspec options, and progress flags. For the secondary fetch, the action passes a precisely constructed refspec that targets the exact commit SHA discovered during verification.

Additionally, src/ref-helper.ts contributes to the process by building the refspecs used throughout the fetch logic, including the logic for handling tag references which bypass the two-step verification. The behavior is validated in __test__/git-source-provider.test.ts, which contains test coverage for the ref-movement detection and re-fetch behavior.

Workflow Configuration and Git Commands

To enable ref-movement handling, configure your workflow with full history fetching:

steps:
  - uses: actions/checkout@v4
    with:
      fetch-depth: 0

When the action detects a moved reference, it executes Git commands equivalent to:


# Initial fetch (full history)

git fetch --no-tags --progress origin refs/heads/main

# If the branch moved after the first fetch:

git fetch --no-tags origin <new-sha>:refs/heads/main

The action's internal logic follows this pattern:

// Simplified from src/git-source-provider.ts
if (settings.fetchDepth <= 0) {
  await git.fetch(refSpec, fetchOptions)        // Initial fetch

  // Detect if the ref now points to a different commit
  const remoteSha = await git.revParse(`${settings.ref}`)
  const localSha  = await git.revParse(`origin/${settings.ref}`)
  if (remoteSha !== localSha) {
    // Re-fetch the exact commit to handle a force-push
    const targetedRef = `${remoteSha}:refs/heads/${settings.ref}`
    await git.fetch(targetedRef, fetchOptions) // Second fetch
  }
}

Summary

  • Two-step verification: The action fetches initially, verifies the ref points to the expected commit, and re-fetches with an exact SHA if movement is detected.
  • Full-history only: The ref-movement check activates exclusively when fetch-depth is 0 or less, as shallow clones fetch specific commits directly.
  • Source locations: Core logic lives in src/git-source-provider.ts with fetch commands implemented in src/git-command-manager.ts and refspec construction in src/ref-helper.ts.
  • Force-push protection: The targeted refspec mechanism ensures the workflow checks out the exact commit requested, even if the branch tip changes between fetch operations.

Frequently Asked Questions

What triggers the secondary fetch in actions/checkout?

The secondary fetch triggers when fetch-depth is set to 0 (full history) and the action detects that the branch reference moved between the initial fetch request and the verification check. This commonly occurs during force-pushes or rapid consecutive commits to the same branch.

Does actions/checkout handle force-pushes correctly?

Yes, when configured with fetch-depth: 0, the action detects force-pushes through its ref-movement verification logic in src/git-source-provider.ts. Upon detecting a mismatch between the expected and actual commit SHA, it executes a second fetch using a targeted refspec that specifies the exact commit SHA, ensuring the correct revision is checked out regardless of the force-push.

Why doesn't ref movement detection apply to shallow clones?

Shallow clones specify exact commit SHAs or limited depth ranges in their initial fetch, which inherently targets specific snapshots rather than moving branch pointers. The ref-movement logic specifically addresses the race condition that exists when fetching symbolic references (branch names) with full history, where the branch tip can change between the start and end of the fetch operation.

Where is the ref movement detection logic implemented?

The detection logic resides in src/git-source-provider.ts within the actions/checkout repository. The method uses GitCommandManager.fetch() from src/git-command-manager.ts to execute Git commands, and leverages src/ref-helper.ts for refspec construction. The specific verification compares the resolved remote SHA against the locally fetched reference to determine if a secondary fetch is necessary.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →