mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-01-12 09:30:28 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
720bd1e96b | ||
|
|
25858c8b16 | ||
|
|
52bab0ba27 | ||
|
|
2e6d0881d9 | ||
|
|
74edacd8e5 | ||
|
|
5d95ba8d85 | ||
|
|
82d500eb7a | ||
|
|
21269c3eed |
@@ -133,3 +133,43 @@ def main(project_list, worktree_list=None, **kwargs):
|
||||
kwargs: Leave this here for forward-compatibility.
|
||||
"""
|
||||
```
|
||||
|
||||
### post-sync
|
||||
|
||||
This hook runs when `repo sync` completes without errors.
|
||||
|
||||
Note: This includes cases where no actual checkout may occur. The hook will still run.
|
||||
For example:
|
||||
- `repo sync -n` performs network fetches only and skips the checkout phase.
|
||||
- `repo sync <project>` only updates the specified project(s).
|
||||
- Partial failures may still result in a successful exit.
|
||||
|
||||
This hook is useful for post-processing tasks such as setting up git hooks,
|
||||
bootstrapping configuration files, or running project initialization logic.
|
||||
|
||||
The hook is defined using the existing `<repo-hooks>` manifest block and is
|
||||
optional. If the hook script fails or is missing, `repo sync` will still
|
||||
complete successfully, and the error will be printed as a warning.
|
||||
|
||||
Example:
|
||||
|
||||
```xml
|
||||
<project name="myorg/dev-tools" path="tools" revision="main" />
|
||||
<repo-hooks in-project="myorg/dev-tools" enabled-list="post-sync">
|
||||
<hook name="post-sync" />
|
||||
</repo-hooks>
|
||||
```
|
||||
|
||||
The `post-sync.py` file should be defined like:
|
||||
|
||||
```py
|
||||
def main(repo_topdir=None, **kwargs):
|
||||
"""Main function invoked directly by repo.
|
||||
|
||||
We must use the name "main" as that is what repo requires.
|
||||
|
||||
Args:
|
||||
repo_topdir: The absolute path to the top-level directory of the repo workspace.
|
||||
kwargs: Leave this here for forward-compatibility.
|
||||
"""
|
||||
```
|
||||
|
||||
1
hooks.py
1
hooks.py
@@ -25,6 +25,7 @@ from git_refs import HEAD
|
||||
# The API we've documented to hook authors. Keep in sync with repo-hooks.md.
|
||||
_API_ARGS = {
|
||||
"pre-upload": {"project_list", "worktree_list"},
|
||||
"post-sync": {"repo_topdir"},
|
||||
}
|
||||
|
||||
|
||||
|
||||
10
progress.py
10
progress.py
@@ -101,6 +101,7 @@ class Progress:
|
||||
self._units = units
|
||||
self._elide = elide and _TTY
|
||||
self._quiet = quiet
|
||||
self._ended = False
|
||||
|
||||
# Only show the active jobs section if we run more than one in parallel.
|
||||
self._show_jobs = False
|
||||
@@ -118,6 +119,11 @@ class Progress:
|
||||
if not quiet and show_elapsed:
|
||||
self._update_thread.start()
|
||||
|
||||
def update_total(self, new_total):
|
||||
"""Updates the total if the new total is larger."""
|
||||
if new_total > self._total:
|
||||
self._total = new_total
|
||||
|
||||
def _update_loop(self):
|
||||
while True:
|
||||
self.update(inc=0)
|
||||
@@ -211,6 +217,10 @@ class Progress:
|
||||
self.update(inc=0)
|
||||
|
||||
def end(self):
|
||||
if self._ended:
|
||||
return
|
||||
self._ended = True
|
||||
|
||||
self._update_event.set()
|
||||
if not _TTY or IsTraceToStderr() or self._quiet:
|
||||
return
|
||||
|
||||
27
project.py
27
project.py
@@ -2061,10 +2061,7 @@ class Project:
|
||||
if head == revid:
|
||||
# Same revision; just update HEAD to point to the new
|
||||
# target branch, but otherwise take no other action.
|
||||
_lwrite(
|
||||
self.work_git.GetDotgitPath(subpath=HEAD),
|
||||
f"ref: {R_HEADS}{name}\n",
|
||||
)
|
||||
self.work_git.SetHead(R_HEADS + name)
|
||||
return True
|
||||
|
||||
GitCommand(
|
||||
@@ -2100,9 +2097,7 @@ class Project:
|
||||
|
||||
revid = self.GetRevisionId(all_refs)
|
||||
if head == revid:
|
||||
_lwrite(
|
||||
self.work_git.GetDotgitPath(subpath=HEAD), "%s\n" % revid
|
||||
)
|
||||
self.work_git.DetachHead(revid)
|
||||
else:
|
||||
self._Checkout(revid, quiet=True)
|
||||
GitCommand(
|
||||
@@ -3492,9 +3487,7 @@ class Project:
|
||||
self._createDotGit(dotgit)
|
||||
|
||||
if init_dotgit:
|
||||
_lwrite(
|
||||
os.path.join(self.gitdir, HEAD), f"{self.GetRevisionId()}\n"
|
||||
)
|
||||
self.work_git.UpdateRef(HEAD, self.GetRevisionId(), detach=True)
|
||||
|
||||
# Finish checking out the worktree.
|
||||
cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
|
||||
@@ -3841,19 +3834,11 @@ class Project:
|
||||
|
||||
def GetHead(self):
|
||||
"""Return the ref that HEAD points to."""
|
||||
path = self.GetDotgitPath(subpath=HEAD)
|
||||
try:
|
||||
with open(path) as fd:
|
||||
line = fd.readline()
|
||||
except OSError as e:
|
||||
return self.rev_parse("--symbolic-full-name", HEAD)
|
||||
except GitError as e:
|
||||
path = self.GetDotgitPath(subpath=HEAD)
|
||||
raise NoManifestException(path, str(e))
|
||||
try:
|
||||
line = line.decode()
|
||||
except AttributeError:
|
||||
pass
|
||||
if line.startswith("ref: "):
|
||||
return line[5:-1]
|
||||
return line[:-1]
|
||||
|
||||
def SetHead(self, ref, message=None):
|
||||
cmdv = []
|
||||
|
||||
@@ -127,6 +127,7 @@ to update the working directory files.
|
||||
return {
|
||||
"REPO_MANIFEST_URL": "manifest_url",
|
||||
"REPO_MIRROR_LOCATION": "reference",
|
||||
"REPO_GIT_LFS": "git_lfs",
|
||||
}
|
||||
|
||||
def _SyncManifest(self, opt):
|
||||
|
||||
146
subcmds/sync.py
146
subcmds/sync.py
@@ -68,6 +68,7 @@ from git_config import GetUrlCookieFile
|
||||
from git_refs import HEAD
|
||||
from git_refs import R_HEADS
|
||||
import git_superproject
|
||||
from hooks import RepoHook
|
||||
import platform_utils
|
||||
from progress import elapsed_str
|
||||
from progress import jobs_str
|
||||
@@ -411,16 +412,18 @@ later is required to fix a server side protocol bug.
|
||||
type=int,
|
||||
metavar="JOBS",
|
||||
help="number of network jobs to run in parallel (defaults to "
|
||||
"--jobs or 1). Ignored when --interleaved is set",
|
||||
"--jobs or 1). Ignored unless --no-interleaved is set",
|
||||
)
|
||||
p.add_option(
|
||||
"--jobs-checkout",
|
||||
default=None,
|
||||
type=int,
|
||||
metavar="JOBS",
|
||||
help="number of local checkout jobs to run in parallel (defaults "
|
||||
f"to --jobs or {DEFAULT_LOCAL_JOBS}). Ignored when --interleaved "
|
||||
"is set",
|
||||
help=(
|
||||
"number of local checkout jobs to run in parallel (defaults "
|
||||
f"to --jobs or {DEFAULT_LOCAL_JOBS}). Ignored unless "
|
||||
"--no-interleaved is set"
|
||||
),
|
||||
)
|
||||
|
||||
p.add_option(
|
||||
@@ -479,7 +482,14 @@ later is required to fix a server side protocol bug.
|
||||
p.add_option(
|
||||
"--interleaved",
|
||||
action="store_true",
|
||||
help="fetch and checkout projects in parallel (experimental)",
|
||||
default=True,
|
||||
help="fetch and checkout projects in parallel (default)",
|
||||
)
|
||||
p.add_option(
|
||||
"--no-interleaved",
|
||||
dest="interleaved",
|
||||
action="store_false",
|
||||
help="fetch and checkout projects in phases",
|
||||
)
|
||||
p.add_option(
|
||||
"-n",
|
||||
@@ -623,6 +633,7 @@ later is required to fix a server side protocol bug.
|
||||
action="store_true",
|
||||
help=optparse.SUPPRESS_HELP,
|
||||
)
|
||||
RepoHook.AddOptionGroup(p, "post-sync")
|
||||
|
||||
def _GetBranch(self, manifest_project):
|
||||
"""Returns the branch name for getting the approved smartsync manifest.
|
||||
@@ -1847,6 +1858,21 @@ later is required to fix a server side protocol bug.
|
||||
except (KeyboardInterrupt, Exception) as e:
|
||||
raise RepoUnhandledExceptionError(e, aggregate_errors=errors)
|
||||
|
||||
# Run post-sync hook only after successful sync
|
||||
self._RunPostSyncHook(opt)
|
||||
|
||||
def _RunPostSyncHook(self, opt):
|
||||
"""Run post-sync hook if configured in manifest <repo-hooks>."""
|
||||
hook = RepoHook.FromSubcmd(
|
||||
hook_type="post-sync",
|
||||
manifest=self.manifest,
|
||||
opt=opt,
|
||||
abort_if_user_denies=False,
|
||||
)
|
||||
success = hook.Run(repo_topdir=self.client.topdir)
|
||||
if not success:
|
||||
print("Warning: post-sync hook reported failure.")
|
||||
|
||||
def _ExecuteHelper(self, opt, args, errors):
|
||||
manifest = self.outer_manifest
|
||||
if not opt.outer_manifest:
|
||||
@@ -2243,51 +2269,57 @@ later is required to fix a server side protocol bug.
|
||||
checkout_finish = None
|
||||
checkout_stderr = ""
|
||||
|
||||
if fetch_success and not opt.network_only:
|
||||
checkout_start = time.time()
|
||||
stderr_capture = io.StringIO()
|
||||
try:
|
||||
with contextlib.redirect_stderr(stderr_capture):
|
||||
syncbuf = SyncBuffer(
|
||||
project.manifest.manifestProject.config,
|
||||
detach_head=opt.detach_head,
|
||||
)
|
||||
local_half_errors = []
|
||||
project.Sync_LocalHalf(
|
||||
syncbuf,
|
||||
force_sync=opt.force_sync,
|
||||
force_checkout=opt.force_checkout,
|
||||
force_rebase=opt.rebase,
|
||||
errors=local_half_errors,
|
||||
verbose=opt.verbose,
|
||||
)
|
||||
checkout_success = syncbuf.Finish()
|
||||
if local_half_errors:
|
||||
checkout_error = SyncError(
|
||||
aggregate_errors=local_half_errors
|
||||
if fetch_success:
|
||||
# We skip checkout if it's network-only or if the project has no
|
||||
# working tree (e.g., a mirror).
|
||||
if opt.network_only or not project.worktree:
|
||||
checkout_success = True
|
||||
else:
|
||||
# This is a normal project that needs a checkout.
|
||||
checkout_start = time.time()
|
||||
stderr_capture = io.StringIO()
|
||||
try:
|
||||
with contextlib.redirect_stderr(stderr_capture):
|
||||
syncbuf = SyncBuffer(
|
||||
project.manifest.manifestProject.config,
|
||||
detach_head=opt.detach_head,
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
logger.error(
|
||||
"Keyboard interrupt while processing %s", project.name
|
||||
)
|
||||
except GitError as e:
|
||||
checkout_error = e
|
||||
logger.error(
|
||||
"error.GitError: Cannot checkout %s: %s", project.name, e
|
||||
)
|
||||
except Exception as e:
|
||||
checkout_error = e
|
||||
logger.error(
|
||||
"error: Cannot checkout %s: %s: %s",
|
||||
project.name,
|
||||
type(e).__name__,
|
||||
e,
|
||||
)
|
||||
finally:
|
||||
checkout_finish = time.time()
|
||||
checkout_stderr = stderr_capture.getvalue()
|
||||
elif fetch_success:
|
||||
checkout_success = True
|
||||
local_half_errors = []
|
||||
project.Sync_LocalHalf(
|
||||
syncbuf,
|
||||
force_sync=opt.force_sync,
|
||||
force_checkout=opt.force_checkout,
|
||||
force_rebase=opt.rebase,
|
||||
errors=local_half_errors,
|
||||
verbose=opt.verbose,
|
||||
)
|
||||
checkout_success = syncbuf.Finish()
|
||||
if local_half_errors:
|
||||
checkout_error = SyncError(
|
||||
aggregate_errors=local_half_errors
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
logger.error(
|
||||
"Keyboard interrupt while processing %s", project.name
|
||||
)
|
||||
except GitError as e:
|
||||
checkout_error = e
|
||||
logger.error(
|
||||
"error.GitError: Cannot checkout %s: %s",
|
||||
project.name,
|
||||
e,
|
||||
)
|
||||
except Exception as e:
|
||||
checkout_error = e
|
||||
logger.error(
|
||||
"error: Cannot checkout %s: %s: %s",
|
||||
project.name,
|
||||
type(e).__name__,
|
||||
e,
|
||||
)
|
||||
finally:
|
||||
checkout_finish = time.time()
|
||||
checkout_stderr = stderr_capture.getvalue()
|
||||
|
||||
# Consolidate all captured output.
|
||||
captured_parts = []
|
||||
@@ -2488,11 +2520,22 @@ later is required to fix a server side protocol bug.
|
||||
|
||||
pending_relpaths = {p.relpath for p in projects_to_sync}
|
||||
if previously_pending_relpaths == pending_relpaths:
|
||||
stalled_projects_str = "\n".join(
|
||||
f" - {path}"
|
||||
for path in sorted(list(pending_relpaths))
|
||||
)
|
||||
logger.error(
|
||||
"Stall detected in interleaved sync, not all "
|
||||
"projects could be synced."
|
||||
"The following projects failed and could not "
|
||||
"be synced:\n%s",
|
||||
stalled_projects_str,
|
||||
)
|
||||
err_event.set()
|
||||
|
||||
# Include these in the final error report.
|
||||
self._interleaved_err_checkout = True
|
||||
self._interleaved_err_checkout_results.extend(
|
||||
list(pending_relpaths)
|
||||
)
|
||||
break
|
||||
previously_pending_relpaths = pending_relpaths
|
||||
|
||||
@@ -2553,6 +2596,7 @@ later is required to fix a server side protocol bug.
|
||||
manifest=manifest,
|
||||
all_manifests=not opt.this_manifest_only,
|
||||
)
|
||||
pm.update_total(len(project_list))
|
||||
finally:
|
||||
sync_event.set()
|
||||
sync_progress_thread.join()
|
||||
|
||||
@@ -309,6 +309,7 @@ class FakeProject:
|
||||
self.relpath = relpath
|
||||
self.name = name or relpath
|
||||
self.objdir = objdir or relpath
|
||||
self.worktree = relpath
|
||||
|
||||
self.use_git_worktrees = False
|
||||
self.UseAlternates = False
|
||||
@@ -836,6 +837,25 @@ class InterleavedSyncTest(unittest.TestCase):
|
||||
project.Sync_NetworkHalf.assert_called_once()
|
||||
project.Sync_LocalHalf.assert_not_called()
|
||||
|
||||
def test_worker_no_worktree(self):
|
||||
"""Test interleaved sync does not checkout with no worktree."""
|
||||
opt = self._get_opts()
|
||||
project = self.projA
|
||||
project.worktree = None
|
||||
project.Sync_NetworkHalf = mock.Mock(
|
||||
return_value=SyncNetworkHalfResult(error=None, remote_fetched=True)
|
||||
)
|
||||
project.Sync_LocalHalf = mock.Mock()
|
||||
self.mock_context["projects"] = [project]
|
||||
|
||||
result_obj = self.cmd._SyncProjectList(opt, [0])
|
||||
result = result_obj.results[0]
|
||||
|
||||
self.assertTrue(result.fetch_success)
|
||||
self.assertTrue(result.checkout_success)
|
||||
project.Sync_NetworkHalf.assert_called_once()
|
||||
project.Sync_LocalHalf.assert_not_called()
|
||||
|
||||
def test_worker_fetch_fails_exception(self):
|
||||
"""Test _SyncProjectList with an exception during fetch."""
|
||||
opt = self._get_opts()
|
||||
|
||||
Reference in New Issue
Block a user