# repo hooks

[TOC]

Repo provides a mechanism to hook specific stages of the runtime with custom
python modules.  All the hooks live in one git project which is checked out by
the manifest (specified during `repo init`), and the manifest itself defines
which hooks are registered.

These are useful to run linters, check formatting, and run quick unittests
before allowing a step to proceed (e.g. before uploading a commit to Gerrit).

A complete example can be found in the Android project.  It can be easily
re-used by any repo based project and is not specific to Android.<br>
https://android.googlesource.com/platform/tools/repohooks

## Approvals

When a hook is processed the first time, the user is prompted for approval.
We don't want to execute arbitrary code without explicit consent.  For manifests
fetched via secure protocols (e.g. https://), the user is prompted once.  For
insecure protocols (e.g. http://), the user is prompted whenever the registered
repohooks project is updated and a hook is triggered.

## Manifest Settings

For the full syntax, see the [repo manifest format](./manifest-format.md).

Here's a short example from
[Android](https://android.googlesource.com/platform/manifest/+/HEAD/default.xml).
The `<project>` line checks out the repohooks git repo to the local
`tools/repohooks/` path.  The `<repo-hooks>` line says to look in the project
with the name `platform/tools/repohooks` for hooks to run during the
`pre-upload` phase.

```xml
<project path="tools/repohooks" name="platform/tools/repohooks" />
<repo-hooks in-project="platform/tools/repohooks" enabled-list="pre-upload" />
```

## Source Layout

The repohooks git repo should have a python file with the same name as the hook.
So if you want to support the `pre-upload` hook, you'll need to create a file
named `pre-upload.py`.  Repo will dynamically load that module when processing
the hook and then call the `main` function in it.

Hooks should have their `main` accept `**kwargs` for future compatibility.

## Runtime

Hook return values are ignored.

Any uncaught exceptions from the hook will cause the step to fail.  This is
intended as a fallback safety check though rather than the normal flow.  If
you want your hook to trigger a failure, it should call `sys.exit()` (after
displaying relevant diagnostics).

Output (stdout & stderr) are not filtered in any way.  Hooks should generally
not be too verbose.  A short summary is nice, and some status information when
long running operations occur, but long/verbose output should be used only if
the hook ultimately fails.

The hook runs from the top level of the repo client where the operation is
started.
For example, if the repo client is under `~/tree/`, then that is where the hook
runs, even if you ran repo in a git repository at `~/tree/src/foo/`, or in a
subdirectory of that git repository in `~/tree/src/foo/bar/`.
Hooks frequently start off by doing a `os.chdir` to the specific project they're
called on (see below) and then changing back to the original dir when they're
finished.

Python's `sys.path` is modified so that the top of repohooks directory comes
first.  This should help simplify the hook logic to easily allow importing of
local modules.

Repo does not modify the state of the git checkout.  This means that the hooks
might be running in a dirty git repo with many commits and checked out to the
latest one.  If the hook wants to operate on specific git commits, it needs to
manually discover the list of pending commits, extract the diff/commit, and
then check it directly.  Hooks should not normally modify the active git repo
(such as checking out a specific commit to run checks) without first prompting
the user.  Although user interaction is discouraged in the common case, it can
be useful when deploying automatic fixes.

### Shebang Handling

*** note
This is intended as a transitional feature.  Hooks are expected to eventually
migrate to Python 3 only as Python 2 is EOL & deprecated.
***

If the hook is written against a specific version of Python (either 2 or 3),
the script can declare that explicitly.  Repo will then attempt to execute it
under the right version of Python regardless of the version repo itself might
be executing under.

Here are the shebangs that are recognized.

* `#!/usr/bin/env python` & `#!/usr/bin/python`: The hook is compatible with
  Python 2 & Python 3.  For maximum compatibility, these are recommended.
* `#!/usr/bin/env python2` & `#!/usr/bin/python2`: The hook requires Python 2.
  Version specific names like `python2.7` are also recognized.
* `#!/usr/bin/env python3` & `#!/usr/bin/python3`: The hook requires Python 3.
  Version specific names like `python3.6` are also recognized.

If no shebang is detected, or does not match the forms above, we assume that the
hook is compatible with both Python 2 & Python 3 as if `#!/usr/bin/python` was
used.

## Hooks

Here are all the points available for hooking.

### pre-upload

This hook runs when people run `repo upload`.

The `pre-upload.py` file should be defined like:

```py
def main(project_list, worktree_list=None, **kwargs):
    """Main function invoked directly by repo.

    We must use the name "main" as that is what repo requires.

    Args:
      project_list: List of projects to run on.
      worktree_list: A list of directories.  It should be the same length as
          project_list, so that each entry in project_list matches with a
          directory in worktree_list.  If None, we will attempt to calculate
          the directories automatically.
      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.
    """
```
