sync: support post-sync hook in <repo-hooks>

Add support for a new hook type "post-sync" declared in the manifest using
<repo-hooks>. This allows executing a script automatically after a successful
`repo sync`.

This is useful for initializing developer environments, installing project-wide
Git hooks, generating configs, and other post-sync automation tasks.

Example manifest usage:

  <project name="myorg/repo-hooks" path="hooks" revision="main" />
  <repo-hooks in-project="myorg/repo-hooks" enabled-list="post-sync">
    <hook name="post-sync" />
  </repo-hooks>

The hook script must be named `post-sync.py` and located at the root of the
hook project.

The post-sync hook does not block `repo sync`; if the script fails, the sync
still completes successfully with a warning.

Test: Added `post-sync.py` in hook project and verified it runs after `repo sync`

Bug: b/421694721
Change-Id: I69f3158f0fc319d73a85028d6e90fea02c1dc8c8
Signed-off-by: Kenny Cheng <chao.shun.cheng.tw@gmail.com>
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/480581
Reviewed-by: Scott Lee <ddoman@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
diff --git a/docs/repo-hooks.md b/docs/repo-hooks.md
index cbb1aac..a56f261 100644
--- a/docs/repo-hooks.md
+++ b/docs/repo-hooks.md
@@ -133,3 +133,43 @@
       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.
+    """
+```
diff --git a/hooks.py b/hooks.py
index f940e3f..fc31a5e 100644
--- a/hooks.py
+++ b/hooks.py
@@ -25,6 +25,7 @@
 # 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"},
 }
 
 
diff --git a/subcmds/sync.py b/subcmds/sync.py
index 20d75dc..250925f 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -68,6 +68,7 @@
 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
@@ -623,6 +624,7 @@
             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 +1849,21 @@
         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: