diff options
| author | Gavin Mak <gavinmak@google.com> | 2026-09-03 22:55:57 +0000 |
|---|---|---|
| committer | gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2026-09-15 12:53:01 -0700 |
| commit | a468ea7752cddd67fa2242ef038ac96f53c35a6a (patch) | |
| tree | 42dd5085036f44d3559a992ee12b465850e5c095 /subcmds | |
| parent | c2c330ba4a92c915f0180457f0d7ee29031bb253 (diff) | |
| download | git-repo-a468ea7752cddd67fa2242ef038ac96f53c35a6a.tar.gz | |
sync: implement reprojectcmd for the local half
Add `repo.reprojectcmd`, the checkout counterpart to `repo.fetchcmd`.
When set, `repo sync` runs this command instead of Git to materialize a
project's index and worktree at the target tree, while `repo` handles
ref updates directly via `git update-ref`.
This replaces Git's tree materialization steps: detaching HEAD,
fast-forwarding, and hard-resetting. Rebasing is not delegated, and the
command is skipped if HEAD is already at the target, if HEAD is ahead of
the target during fast-forward, or for MetaProjects.
The command runs in a subshell with project environment variables (such
as `REPO_TREV`). Before running, `repo` ensures no operation is in
progress and no staged changes exist. Worktree collision detection is
delegated to the command (preserving benign unstaged/untracked edits).
Afterward, `repo` verifies that HEAD was untouched and that the index
matches the target tree.
Like `repo.fetchcmd`, this requires `repo.uselocalgitdirs`. Nested
projects and submodules are unsupported; `repo sync` fails if the
manifest contains any while `repo.reprojectcmd` is enabled.
Verified end-to-end with repo init using local-gitdirs, repo.fetchcmd,
and repo.reprojectcmd ('git -C $REPO_PATH read-tree -m -u $REPO_TREV'):
* Verified detached HEAD checkout and correct reflog generation across
projects.
* Verified benign unstaged edits and untracked files survive checkout.
* Verified conflicting untracked files fail with exit 128 without
clobbering worktree.
* Verified staged changes fail upfront before reprojectcmd is executed.
Bug: 513329573
Change-Id: I964d24d22dccffc05a9b991ad69f3a7e93268c01
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/626281
Tested-by: Gavin Mak <gavinmak@google.com>
Reviewed-by: Brian Gan <brgan@google.com>
Commit-Queue: Gavin Mak <gavinmak@google.com>
Diffstat (limited to 'subcmds')
| -rw-r--r-- | subcmds/sync.py | 47 |
1 files changed, 47 insertions, 0 deletions
diff --git a/subcmds/sync.py b/subcmds/sync.py index d88ec883e..c51ecf385 100644 --- a/subcmds/sync.py +++ b/subcmds/sync.py | |||
| @@ -72,6 +72,7 @@ from git_refs import HEAD | |||
| 72 | from git_refs import R_HEADS | 72 | from git_refs import R_HEADS |
| 73 | import git_superproject | 73 | import git_superproject |
| 74 | from hooks import RepoHook | 74 | from hooks import RepoHook |
| 75 | from manifest_xml import XmlManifest | ||
| 75 | import platform_utils | 76 | import platform_utils |
| 76 | from progress import elapsed_str | 77 | from progress import elapsed_str |
| 77 | from progress import jobs_str | 78 | from progress import jobs_str |
| @@ -156,6 +157,11 @@ def _SafeCheckoutOrder(checkouts: List[Project]) -> List[List[Project]]: | |||
| 156 | return res | 157 | return res |
| 157 | 158 | ||
| 158 | 159 | ||
| 160 | def _NestedProjects(projects: List[Project]) -> List[Project]: | ||
| 161 | """Return the projects in |projects| living inside another one's path.""" | ||
| 162 | return [p for level in _SafeCheckoutOrder(projects)[1:] for p in level] | ||
| 163 | |||
| 164 | |||
| 159 | def _ParentFirstBatches(projects: List[Project]) -> List[List[Project]]: | 165 | def _ParentFirstBatches(projects: List[Project]) -> List[List[Project]]: |
| 160 | """Group |projects| so that a parent is fetched before its submodules. | 166 | """Group |projects| so that a parent is fetched before its submodules. |
| 161 | 167 | ||
| @@ -2478,6 +2484,7 @@ later is required to fix a server side protocol bug. | |||
| 2478 | manifest=manifest, | 2484 | manifest=manifest, |
| 2479 | all_manifests=not opt.this_manifest_only, | 2485 | all_manifests=not opt.this_manifest_only, |
| 2480 | ) | 2486 | ) |
| 2487 | self._CheckReprojectCmdNesting(opt, args, manifest, all_projects) | ||
| 2481 | 2488 | ||
| 2482 | # Log the repo projects by existing and new. | 2489 | # Log the repo projects by existing and new. |
| 2483 | existing = [x for x in all_projects if x.Exists] | 2490 | existing = [x for x in all_projects if x.Exists] |
| @@ -2541,6 +2548,46 @@ later is required to fix a server side protocol bug. | |||
| 2541 | if not opt.quiet: | 2548 | if not opt.quiet: |
| 2542 | print("repo sync has finished successfully.") | 2549 | print("repo sync has finished successfully.") |
| 2543 | 2550 | ||
| 2551 | def _CheckReprojectCmdNesting( | ||
| 2552 | self, | ||
| 2553 | opt: optparse.Values, | ||
| 2554 | args: List[str], | ||
| 2555 | manifest: XmlManifest, | ||
| 2556 | all_projects: List[Project], | ||
| 2557 | ) -> None: | ||
| 2558 | """Fail when repo.reprojectcmd is used on a manifest nesting projects. | ||
| 2559 | |||
| 2560 | The command materializes a project's tree without Git, so nothing | ||
| 2561 | keeps it from clobbering a project or submodule checked out inside | ||
| 2562 | that tree. See docs/reproject-cmd.md. | ||
| 2563 | """ | ||
| 2564 | if not any(p.UseReprojectCmd for p in all_projects): | ||
| 2565 | return | ||
| 2566 | projects = all_projects | ||
| 2567 | if args: | ||
| 2568 | # Nesting is a property of the manifest, not of the projects | ||
| 2569 | # picked on the command line. | ||
| 2570 | projects = self.GetProjects( | ||
| 2571 | [], | ||
| 2572 | groups=opt.groups, | ||
| 2573 | missing_ok=True, | ||
| 2574 | submodules_ok=opt.recurse_submodules, | ||
| 2575 | manifest=manifest, | ||
| 2576 | all_manifests=not opt.this_manifest_only, | ||
| 2577 | ) | ||
| 2578 | nested = _NestedProjects(projects) | ||
| 2579 | if not nested: | ||
| 2580 | return | ||
| 2581 | e = SyncError( | ||
| 2582 | "error: repo.reprojectcmd does not support nested projects or " | ||
| 2583 | "submodules; found:\n" | ||
| 2584 | + "\n".join( | ||
| 2585 | f" - {p.RelPath(local=opt.this_manifest_only)}" for p in nested | ||
| 2586 | ) | ||
| 2587 | ) | ||
| 2588 | logger.error(e) | ||
| 2589 | raise e | ||
| 2590 | |||
| 2544 | def _CreateSyncProgressThread( | 2591 | def _CreateSyncProgressThread( |
| 2545 | self, pm: Progress, stop_event: _threading.Event | 2592 | self, pm: Progress, stop_event: _threading.Event |
| 2546 | ) -> _threading.Thread: | 2593 | ) -> _threading.Thread: |
