After repo.reprojectcmd exits 0, _Reproject ran `git diff-index --cached
--name-only <revid>^{tree}` to verify that the index matches the target
commit's tree.
Peeling <revid>^{tree} and comparing every entry against the index
forces Git to read the target commit and walk every tree object in the
target revision from .git/objects.
reprojectcmd exists for virtualized filesystems such as ABFS whose
checkout command materializes the worktree and writes .git/index from
its own tree metadata without reading .git/objects/pack/*-trees-*.pack.
Running diff-index against <revid>^{tree} afterward forces Git to fault
in the tree packfiles over the network (260 FetchTrees pack downloads /
1.64 GiB across a 3,296-project Android checkout).
Trust reprojectcmd's exit status for index materialization instead of
re-walking the target tree in Git, while keeping the HEAD postcondition
check that verifies the command did not modify refs.
Bug: 543101585
Change-Id: Ie5feea41a4191245232c6763a489d01596370c55
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/643004
Commit-Queue: Gavin Mak <gavinmak@google.com>
Tested-by: Gavin Mak <gavinmak@google.com>
Reviewed-by: Brian Gan <brgan@google.com>
5.4 KiB
Reproject Command Contract
The repo.reprojectcmd configuration names a command that repo sync runs
instead of Git to move a project's index and worktree to the tree of the
target commit. It is the checkout-side counterpart of repo.fetchcmd (see
docs/fetch-cmd.md): together they let an external tool take over both the
network fetch and the materialization of a project. This is useful on
virtualized filesystems that address content by hash, where a tree can be
materialized far faster than git checkout can write every file.
The command only materializes the tree. repo then makes the ref write that
Git would have made, using git update-ref.
Configuration
To use this feature, set the following in .repo/manifests.git/config:
[repo]
reprojectcmd = "your custom command here"
uselocalgitdirs = true
Setting repo.reprojectcmd requires repo.uselocalgitdirs to be set to
true.
For reference, this command does with Git what repo would otherwise do
itself:
[repo]
reprojectcmd = "git -C $REPO_PATH read-tree -m -u $REPO_TREV"
uselocalgitdirs = true
The one-tree merge applies the change to the target, keeps local changes to every other path, and refuses to overwrite a modified or untracked file, so it enforces the preconditions below by itself. It also works for a project that has nothing checked out yet.
Environment Variables
The command is executed in a subshell, from the root of the client, populated
with standard project-context environment variables. For details on standard
variables (such as REPO_PROJECT, REPO_PATH, REPO_REMOTE, etc.), see the
Environment section in repo help forall or subcmds/forall.py.
The variables the command typically needs are:
REPO_PATH: The project path relative to the root of the client.REPO_TREV: The target revision resolved to a full commit hash. Match this commit's tree.
There is no force mode: a project that would need one never reaches the command (see the preconditions below).
When the command runs
repo sync already classifies each project and picks a Git operation. The
command replaces the three that are a materialization of a target tree:
- The checkout that detaches HEAD at the target. This is the common case: a
project on a detached HEAD, a project on a branch that does not track
upstream, and
repo sync -d. - The fast-forward of the checked out branch to the target.
- The hard reset of the checked out branch to the target, when the commits it carried were dropped upstream.
After the command exits 0, repo writes the ref itself: it detaches HEAD at
REPO_TREV, or moves the checked out branch to REPO_TREV.
The command is not run:
- When
HEADalready namesREPO_TREV. - At the fast-forward step when
HEADis ahead ofREPO_TREV, where Git's merge would be a no-op. - For a rebase. A branch carrying local commits has them replayed onto the
target by
git rebase, which is not a materialization of a target tree. - For
MetaProjects (i.e. the internalreporepository itself at.repo/repoand themanifestsrepository at.repo/manifests).
Contract
Preconditions
Before invoking the command, repo ensures that:
- The index has no staged changes (the index matches
HEAD, or is empty on an unbornHEAD). - No rebase, cherry-pick, merge, or revert is in progress.
Detecting collisions with untracked files or unstaged working-tree modifications is the responsibility of the reproject command itself (e.g. via git read-tree -m -u $REPO_TREV or a custom virtual filesystem checkout tool). If local changes collide with the target tree, the command must abort with a non-zero exit code. Local modifications and untracked files outside the diff between HEAD and REPO_TREV must be preserved.
Postconditions on exit 0
After the command exits with status 0, repo expects the following
postconditions to be met:
- The index matches
REPO_TREV's tree (repotrusts the command's exit status and does not rungit diff-indexagainstREPO_TREV^{tree}, which would read every tree object from.git/objects). HEADstill names what it did before the command, and its resolved commit object ID has not changed.
Invariants
The command may modify the worktree and the index, and may write project-local Git config. The command must:
- Apply the change from
HEAD's tree toREPO_TREV's tree and leave every other path alone. Local modifications and untracked files outside that change must survive: the command applies a diff, it does not reset the tree. - Not write any ref, including
HEADandORIG_HEAD.repoowns every ref write. - Not create or replace
.git/, and not touch anything under.repo/. - Not require the Git remote, to preserve
repo sync --local-only. - Be idempotent. Running it twice on the same target is a no-op.
Failure
- A non-zero exit status, a failed precondition or a failed postcondition fails that project's sync, and the command's or Git's output is surfaced to the user.
- Other projects continue, and
repo syncexits non-zero.
Limitations
Nested projects are out of scope: a project whose path lies inside another
project's path, a <project> nested in another <project> in the manifest,
and a submodule discovered with sync-s or --recurse-submodules. repo sync
fails if the manifest has one while repo.reprojectcmd is set.