├── man
├── repo-branch.1
├── repo-version.1
├── repo-prune.1
├── repo-stage.1
├── repo-cherry-pick.1
├── repo-gc.1
├── repo-help.1
├── repo-selfupdate.1
├── repo-checkout.1
├── repo-diff.1
├── repo-abandon.1
├── repo-info.1
├── repo-start.1
├── repo-overview.1
├── repo-download.1
├── repo-wipe.1
├── repo-rebase.1
├── repo-branches.1
├── repo-list.1
├── repo-diffmanifests.1
├── repo.1
├── repo-status.1
├── repo-grep.1
├── repo-smartsync.1
└── repo-forall.1
├── constraints.txt
├── tests
├── fixtures
│ ├── .gitignore
│ └── test.gitconfig
├── test_update_manpages.py
├── test_subcmds_init.py
├── test_editor.py
├── test_platform_utils.py
├── test_hooks.py
├── test_repo_trace.py
├── test_error.py
├── test_subcmds_upload.py
├── test_color.py
├── conftest.py
├── test_ssh.py
├── test_repo_logging.py
└── test_subcmds_manifest.py
├── .gitreview
├── MANIFEST.in
├── .gitattributes
├── release
├── README.md
├── update-manpages
├── util.py
├── update-hooks
├── check-metadata.py
└── sign-tag.py
├── .gitignore
├── .project
├── .pydevproject
├── .flake8
├── .github
└── workflows
│ ├── flake8-postsubmit.yml
│ ├── close-pull-request.yml
│ └── test-ci.yml
├── git_ssh
├── pyproject.toml
├── .mailmap
├── subcmds
├── smartsync.py
├── __init__.py
├── selfupdate.py
├── version.py
├── diff.py
├── prune.py
├── overview.py
├── checkout.py
├── stage.py
├── cherry_pick.py
├── list.py
└── start.py
├── wrapper.py
├── .isort.cfg
├── hooks
├── pre-auto-gc
└── commit-msg
├── run_tests.vpython3.8
├── tox.ini
├── git_trace2_event_log.py
├── fetch.py
├── README.md
├── setup.py
├── requirements.json
├── run_tests.vpython3
├── repo_logging.py
├── editor.py
├── pager.py
├── docs
└── python-support.md
├── completion.bash
├── run_tests
├── git_refs.py
└── repo_trace.py
/man/repo-branch.1:
--------------------------------------------------------------------------------
1 | .so man1/repo-branches.1
--------------------------------------------------------------------------------
/constraints.txt:
--------------------------------------------------------------------------------
1 | # NB: Keep in sync with run_tests.vpython3.
2 | black<26
3 |
--------------------------------------------------------------------------------
/tests/fixtures/.gitignore:
--------------------------------------------------------------------------------
1 | /.repo_not.present.gitconfig.json
2 | /.repo_test.gitconfig.json
3 |
--------------------------------------------------------------------------------
/.gitreview:
--------------------------------------------------------------------------------
1 | [gerrit]
2 | host=gerrit-review.googlesource.com
3 | scheme=https
4 | project=git-repo.git
5 | defaultbranch=main
6 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | graft docs hooks tests
2 | include *.py
3 | include LICENSE
4 | include git_ssh
5 | include repo
6 | include run_tests
7 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Prevent /bin/sh scripts from being clobbered by autocrlf=true
2 | git_ssh text eol=lf
3 | repo text eol=lf
4 | hooks/* text eol=lf
5 |
--------------------------------------------------------------------------------
/release/README.md:
--------------------------------------------------------------------------------
1 | These are helper tools for managing official releases.
2 | See the [release process](../docs/release-process.md) document for more details.
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.asc
2 | *.egg-info/
3 | *.log
4 | *.pyc
5 | __pycache__
6 | /dist
7 | .repopickle_*
8 | /repoc
9 | /.tox
10 | /.venv
11 |
12 | # PyCharm related
13 | /.idea/
14 |
--------------------------------------------------------------------------------
/tests/fixtures/test.gitconfig:
--------------------------------------------------------------------------------
1 | [section]
2 | empty
3 | nonempty = true
4 | boolinvalid = oops
5 | booltrue = true
6 | boolfalse = false
7 | intinvalid = oops
8 | inthex = 0x10
9 | inthexk = 0x10k
10 | int = 10
11 | intk = 10k
12 | intm = 10m
13 | intg = 10g
14 |
15 | [color "status"]
16 | one = yellow
17 | two = magenta cyan
18 | three = black red ul
19 | reset = reset
20 | none
21 | empty =
22 |
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | git-repo
4 |
5 |
6 |
7 |
8 |
9 | org.python.pydev.PyDevBuilder
10 |
11 |
12 |
13 |
14 |
15 | org.python.pydev.pythonNature
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.pydevproject:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | /git-repo
7 |
8 | python 2.7
9 | Default
10 |
11 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 80
3 | per-file-ignores =
4 | # E501: line too long
5 | tests/test_git_superproject.py: E501
6 | extend-ignore =
7 | # E203: Whitespace before ':'
8 | # See https://github.com/PyCQA/pycodestyle/issues/373
9 | E203,
10 | # E402: Module level import not at top of file
11 | E402,
12 | # E731: do not assign a lambda expression, use a def
13 | E731,
14 | exclude =
15 | .venv,
16 | venv,
17 | .tox,
18 |
--------------------------------------------------------------------------------
/.github/workflows/flake8-postsubmit.yml:
--------------------------------------------------------------------------------
1 | # GitHub actions workflow.
2 | # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions
3 | # https://github.com/marketplace/actions/python-flake8
4 |
5 | name: Flake8
6 |
7 | on:
8 | push:
9 | branches: [main]
10 |
11 | jobs:
12 | lint:
13 | name: Python Lint
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v3
17 | - uses: actions/setup-python@v4
18 | with:
19 | python-version: "3.9"
20 | - name: Run flake8
21 | uses: julianwachholz/flake8-action@v2
22 | with:
23 | checkName: "Python Lint"
24 |
--------------------------------------------------------------------------------
/git_ssh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # Copyright (C) 2009 The Android Open Source Project
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | exec ssh -o "ControlMaster no" -o "ControlPath $REPO_SSH_SOCK" "$@"
17 |
--------------------------------------------------------------------------------
/.github/workflows/close-pull-request.yml:
--------------------------------------------------------------------------------
1 | # GitHub actions workflow.
2 | # https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions
3 |
4 | # https://github.com/superbrothers/close-pull-request
5 | name: Close Pull Request
6 |
7 | on:
8 | pull_request_target:
9 | types: [opened]
10 |
11 | jobs:
12 | run:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: superbrothers/close-pull-request@v3
16 | with:
17 | comment: >
18 | Thanks for your contribution!
19 | Unfortunately, we don't use GitHub pull requests to manage code
20 | contributions to this repository.
21 | Instead, please see [README.md](../blob/HEAD/CONTRIBUTING.md)
22 | which provides full instructions on how to get involved.
23 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2023 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | [tool.black]
16 | line-length = 80
17 | # NB: Keep in sync with tox.ini.
18 | target-version = ['py36', 'py37', 'py38', 'py39', 'py310', 'py311'] #, 'py312'
19 |
20 | [tool.pytest.ini_options]
21 | markers = """
22 | skip_cq: Skip tests in the CQ. Should be rarely used!
23 | """
24 |
--------------------------------------------------------------------------------
/release/update-manpages:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # Copyright (C) 2021 The Android Open Source Project
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | """Helper tool for generating manual page for all repo commands.
17 |
18 | This is intended to be run before every official Repo release.
19 | """
20 |
21 | import sys
22 |
23 | import update_manpages
24 |
25 |
26 | sys.exit(update_manpages.main(sys.argv[1:]))
27 |
--------------------------------------------------------------------------------
/man/repo-version.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "July 2022" "repo version" "Repo Manual"
3 | .SH NAME
4 | repo \- repo version - manual page for repo version
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,version\/\fR
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | Display the version of repo
12 | .SH OPTIONS
13 | .TP
14 | \fB\-h\fR, \fB\-\-help\fR
15 | show this help message and exit
16 | .SS Logging options:
17 | .TP
18 | \fB\-v\fR, \fB\-\-verbose\fR
19 | show all output
20 | .TP
21 | \fB\-q\fR, \fB\-\-quiet\fR
22 | only show errors
23 | .SS Multi\-manifest options:
24 | .TP
25 | \fB\-\-outer\-manifest\fR
26 | operate starting at the outermost manifest
27 | .TP
28 | \fB\-\-no\-outer\-manifest\fR
29 | do not operate on outer manifests
30 | .TP
31 | \fB\-\-this\-manifest\-only\fR
32 | only operate on this (sub)manifest
33 | .TP
34 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
35 | operate on this manifest and its submanifests
36 | .PP
37 | Run `repo help version` to view the detailed manual.
38 |
--------------------------------------------------------------------------------
/.github/workflows/test-ci.yml:
--------------------------------------------------------------------------------
1 | # GitHub actions workflow.
2 | # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions
3 |
4 | name: Test CI
5 |
6 | on:
7 | push:
8 | branches: [main, repo-1, stable, maint]
9 | tags: [v*]
10 |
11 | jobs:
12 | test:
13 | strategy:
14 | fail-fast: false
15 | matrix:
16 | # ubuntu-20.04 is the last version that supports python 3.6
17 | os: [ubuntu-20.04, macos-latest, windows-latest]
18 | python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12']
19 | runs-on: ${{ matrix.os }}
20 |
21 | steps:
22 | - uses: actions/checkout@v3
23 | - name: Set up Python ${{ matrix.python-version }}
24 | uses: actions/setup-python@v4
25 | with:
26 | python-version: ${{ matrix.python-version }}
27 | - name: Install dependencies
28 | run: |
29 | python -m pip install --upgrade pip
30 | python -m pip install tox tox-gh-actions
31 | - name: Test with tox
32 | run: tox
33 |
--------------------------------------------------------------------------------
/.mailmap:
--------------------------------------------------------------------------------
1 | Anthony Newnam Anthony
2 | He Ping heping
3 | Hu Xiuyun Hu xiuyun
4 | Hu Xiuyun Hu Xiuyun
5 | Jelly Chen chenguodong
6 | Jia Bi bijia
7 | Jiri Tyr Jiri tyr
8 | JoonCheol Park Jooncheol Park
9 | Sergii Pylypenko pelya
10 | Shawn Pearce Shawn O. Pearce
11 | Ulrik Sjölin Ulrik Sjolin
12 | Ulrik Sjölin Ulrik Sjolin
13 | Ulrik Sjölin Ulrik Sjölin
14 |
--------------------------------------------------------------------------------
/tests/test_update_manpages.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2022 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Unittests for the update_manpages module."""
16 |
17 | import unittest
18 |
19 | from release import update_manpages
20 |
21 |
22 | class UpdateManpagesTest(unittest.TestCase):
23 | """Tests the update-manpages code."""
24 |
25 | def test_replace_regex(self):
26 | """Check that replace_regex works."""
27 | data = "\n\033[1mSummary\033[m\n"
28 | self.assertEqual(update_manpages.replace_regex(data), "\nSummary\n")
29 |
--------------------------------------------------------------------------------
/subcmds/smartsync.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2010 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from subcmds.sync import Sync
16 |
17 |
18 | class Smartsync(Sync):
19 | COMMON = True
20 | helpSummary = "Update working tree to the latest known good revision"
21 | helpUsage = """
22 | %prog [...]
23 | """
24 | helpDescription = """
25 | The '%prog' command is a shortcut for sync -s.
26 | """
27 |
28 | def _Options(self, p):
29 | Sync._Options(self, p, show_smart=False)
30 |
31 | def Execute(self, opt, args):
32 | opt.smart_sync = True
33 | Sync.Execute(self, opt, args)
34 |
--------------------------------------------------------------------------------
/man/repo-prune.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "July 2022" "repo prune" "Repo Manual"
3 | .SH NAME
4 | repo \- repo prune - manual page for repo prune
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,prune \/\fR[\fI\,\/\fR...]
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | Prune (delete) already merged topics
12 | .SH OPTIONS
13 | .TP
14 | \fB\-h\fR, \fB\-\-help\fR
15 | show this help message and exit
16 | .TP
17 | \fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
18 | number of jobs to run in parallel (default: based on
19 | number of CPU cores)
20 | .SS Logging options:
21 | .TP
22 | \fB\-v\fR, \fB\-\-verbose\fR
23 | show all output
24 | .TP
25 | \fB\-q\fR, \fB\-\-quiet\fR
26 | only show errors
27 | .SS Multi\-manifest options:
28 | .TP
29 | \fB\-\-outer\-manifest\fR
30 | operate starting at the outermost manifest
31 | .TP
32 | \fB\-\-no\-outer\-manifest\fR
33 | do not operate on outer manifests
34 | .TP
35 | \fB\-\-this\-manifest\-only\fR
36 | only operate on this (sub)manifest
37 | .TP
38 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
39 | operate on this manifest and its submanifests
40 | .PP
41 | Run `repo help prune` to view the detailed manual.
42 |
--------------------------------------------------------------------------------
/man/repo-stage.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "July 2022" "repo stage" "Repo Manual"
3 | .SH NAME
4 | repo \- repo stage - manual page for repo stage
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,stage -i \/\fR[\fI\,\/\fR...]
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | Stage file(s) for commit
12 | .SH OPTIONS
13 | .TP
14 | \fB\-h\fR, \fB\-\-help\fR
15 | show this help message and exit
16 | .SS Logging options:
17 | .TP
18 | \fB\-v\fR, \fB\-\-verbose\fR
19 | show all output
20 | .TP
21 | \fB\-q\fR, \fB\-\-quiet\fR
22 | only show errors
23 | .TP
24 | \fB\-i\fR, \fB\-\-interactive\fR
25 | use interactive staging
26 | .SS Multi\-manifest options:
27 | .TP
28 | \fB\-\-outer\-manifest\fR
29 | operate starting at the outermost manifest
30 | .TP
31 | \fB\-\-no\-outer\-manifest\fR
32 | do not operate on outer manifests
33 | .TP
34 | \fB\-\-this\-manifest\-only\fR
35 | only operate on this (sub)manifest
36 | .TP
37 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
38 | operate on this manifest and its submanifests
39 | .PP
40 | Run `repo help stage` to view the detailed manual.
41 | .SH DETAILS
42 | .PP
43 | The 'repo stage' command stages files to prepare the next commit.
44 |
--------------------------------------------------------------------------------
/man/repo-cherry-pick.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "July 2022" "repo cherry-pick" "Repo Manual"
3 | .SH NAME
4 | repo \- repo cherry-pick - manual page for repo cherry-pick
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,cherry-pick \/\fR
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | Cherry\-pick a change.
12 | .SH OPTIONS
13 | .TP
14 | \fB\-h\fR, \fB\-\-help\fR
15 | show this help message and exit
16 | .SS Logging options:
17 | .TP
18 | \fB\-v\fR, \fB\-\-verbose\fR
19 | show all output
20 | .TP
21 | \fB\-q\fR, \fB\-\-quiet\fR
22 | only show errors
23 | .SS Multi\-manifest options:
24 | .TP
25 | \fB\-\-outer\-manifest\fR
26 | operate starting at the outermost manifest
27 | .TP
28 | \fB\-\-no\-outer\-manifest\fR
29 | do not operate on outer manifests
30 | .TP
31 | \fB\-\-this\-manifest\-only\fR
32 | only operate on this (sub)manifest
33 | .TP
34 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
35 | operate on this manifest and its submanifests
36 | .PP
37 | Run `repo help cherry\-pick` to view the detailed manual.
38 | .SH DETAILS
39 | .PP
40 | \&'repo cherry\-pick' cherry\-picks a change from one branch to another. The change
41 | id will be updated, and a reference to the old change id will be added.
42 |
--------------------------------------------------------------------------------
/wrapper.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2014 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import functools
16 | import importlib.machinery
17 | import importlib.util
18 | import os
19 |
20 |
21 | def WrapperDir():
22 | return os.path.dirname(__file__)
23 |
24 |
25 | def WrapperPath():
26 | return os.path.join(WrapperDir(), "repo")
27 |
28 |
29 | @functools.lru_cache(maxsize=None)
30 | def Wrapper():
31 | modname = "wrapper"
32 | loader = importlib.machinery.SourceFileLoader(modname, WrapperPath())
33 | spec = importlib.util.spec_from_loader(modname, loader)
34 | module = importlib.util.module_from_spec(spec)
35 | loader.exec_module(module)
36 | return module
37 |
--------------------------------------------------------------------------------
/man/repo-gc.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "April 2025" "repo gc" "Repo Manual"
3 | .SH NAME
4 | repo \- repo gc - manual page for repo gc
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,gc\/\fR
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | Cleaning up internal repo and Git state.
12 | .SH OPTIONS
13 | .TP
14 | \fB\-h\fR, \fB\-\-help\fR
15 | show this help message and exit
16 | .TP
17 | \fB\-n\fR, \fB\-\-dry\-run\fR
18 | do everything except actually delete
19 | .TP
20 | \fB\-y\fR, \fB\-\-yes\fR
21 | answer yes to all safe prompts
22 | .TP
23 | \fB\-\-repack\fR
24 | repack all projects that use partial clone with
25 | filter=blob:none
26 | .SS Logging options:
27 | .TP
28 | \fB\-v\fR, \fB\-\-verbose\fR
29 | show all output
30 | .TP
31 | \fB\-q\fR, \fB\-\-quiet\fR
32 | only show errors
33 | .SS Multi\-manifest options:
34 | .TP
35 | \fB\-\-outer\-manifest\fR
36 | operate starting at the outermost manifest
37 | .TP
38 | \fB\-\-no\-outer\-manifest\fR
39 | do not operate on outer manifests
40 | .TP
41 | \fB\-\-this\-manifest\-only\fR
42 | only operate on this (sub)manifest
43 | .TP
44 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
45 | operate on this manifest and its submanifests
46 | .PP
47 | Run `repo help gc` to view the detailed manual.
48 |
--------------------------------------------------------------------------------
/man/repo-help.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "July 2022" "repo help" "Repo Manual"
3 | .SH NAME
4 | repo \- repo help - manual page for repo help
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,help \/\fR[\fI\,--all|command\/\fR]
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | Display detailed help on a command
12 | .SH OPTIONS
13 | .TP
14 | \fB\-h\fR, \fB\-\-help\fR
15 | show this help message and exit
16 | .TP
17 | \fB\-a\fR, \fB\-\-all\fR
18 | show the complete list of commands
19 | .TP
20 | \fB\-\-help\-all\fR
21 | show the \fB\-\-help\fR of all commands
22 | .SS Logging options:
23 | .TP
24 | \fB\-v\fR, \fB\-\-verbose\fR
25 | show all output
26 | .TP
27 | \fB\-q\fR, \fB\-\-quiet\fR
28 | only show errors
29 | .SS Multi\-manifest options:
30 | .TP
31 | \fB\-\-outer\-manifest\fR
32 | operate starting at the outermost manifest
33 | .TP
34 | \fB\-\-no\-outer\-manifest\fR
35 | do not operate on outer manifests
36 | .TP
37 | \fB\-\-this\-manifest\-only\fR
38 | only operate on this (sub)manifest
39 | .TP
40 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
41 | operate on this manifest and its submanifests
42 | .PP
43 | Run `repo help help` to view the detailed manual.
44 | .SH DETAILS
45 | .PP
46 | Displays detailed usage information about a command.
47 |
--------------------------------------------------------------------------------
/man/repo-selfupdate.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "July 2022" "repo selfupdate" "Repo Manual"
3 | .SH NAME
4 | repo \- repo selfupdate - manual page for repo selfupdate
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,selfupdate\/\fR
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | Update repo to the latest version
12 | .SH OPTIONS
13 | .TP
14 | \fB\-h\fR, \fB\-\-help\fR
15 | show this help message and exit
16 | .SS Logging options:
17 | .TP
18 | \fB\-v\fR, \fB\-\-verbose\fR
19 | show all output
20 | .TP
21 | \fB\-q\fR, \fB\-\-quiet\fR
22 | only show errors
23 | .SS Multi\-manifest options:
24 | .TP
25 | \fB\-\-outer\-manifest\fR
26 | operate starting at the outermost manifest
27 | .TP
28 | \fB\-\-no\-outer\-manifest\fR
29 | do not operate on outer manifests
30 | .TP
31 | \fB\-\-this\-manifest\-only\fR
32 | only operate on this (sub)manifest
33 | .TP
34 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
35 | operate on this manifest and its submanifests
36 | .SS repo Version options:
37 | .TP
38 | \fB\-\-no\-repo\-verify\fR
39 | do not verify repo source code
40 | .PP
41 | Run `repo help selfupdate` to view the detailed manual.
42 | .SH DETAILS
43 | .PP
44 | The 'repo selfupdate' command upgrades repo to the latest version, if a newer
45 | version is available.
46 | .PP
47 | Normally this is done automatically by 'repo sync' and does not need to be
48 | performed by an end\-user.
49 |
--------------------------------------------------------------------------------
/.isort.cfg:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2023 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | # Config file for the isort python module.
16 | # This is used to enforce import sorting standards.
17 | #
18 | # https://pycqa.github.io/isort/docs/configuration/options.html
19 |
20 | [settings]
21 | # Be compatible with `black` since it also matches what we want.
22 | profile = black
23 |
24 | line_length = 80
25 | length_sort = false
26 | force_single_line = true
27 | lines_after_imports = 2
28 | from_first = false
29 | case_sensitive = false
30 | force_sort_within_sections = true
31 | order_by_type = false
32 |
33 | # Ignore generated files.
34 | extend_skip_glob = *_pb2.py
35 |
36 | # Allow importing multiple classes on a single line from these modules.
37 | # https://google.github.io/styleguide/pyguide#s2.2-imports
38 | single_line_exclusions =
39 | abc,
40 | collections.abc,
41 | typing,
42 |
--------------------------------------------------------------------------------
/man/repo-checkout.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "July 2022" "repo checkout" "Repo Manual"
3 | .SH NAME
4 | repo \- repo checkout - manual page for repo checkout
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,checkout \/\fR[\fI\,\/\fR...]
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | Checkout a branch for development
12 | .SH OPTIONS
13 | .TP
14 | \fB\-h\fR, \fB\-\-help\fR
15 | show this help message and exit
16 | .TP
17 | \fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
18 | number of jobs to run in parallel (default: based on
19 | number of CPU cores)
20 | .SS Logging options:
21 | .TP
22 | \fB\-v\fR, \fB\-\-verbose\fR
23 | show all output
24 | .TP
25 | \fB\-q\fR, \fB\-\-quiet\fR
26 | only show errors
27 | .SS Multi\-manifest options:
28 | .TP
29 | \fB\-\-outer\-manifest\fR
30 | operate starting at the outermost manifest
31 | .TP
32 | \fB\-\-no\-outer\-manifest\fR
33 | do not operate on outer manifests
34 | .TP
35 | \fB\-\-this\-manifest\-only\fR
36 | only operate on this (sub)manifest
37 | .TP
38 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
39 | operate on this manifest and its submanifests
40 | .PP
41 | Run `repo help checkout` to view the detailed manual.
42 | .SH DETAILS
43 | .PP
44 | The 'repo checkout' command checks out an existing branch that was previously
45 | created by 'repo start'.
46 | .PP
47 | The command is equivalent to:
48 | .IP
49 | repo forall [...] \fB\-c\fR git checkout
50 |
--------------------------------------------------------------------------------
/man/repo-diff.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "July 2022" "repo diff" "Repo Manual"
3 | .SH NAME
4 | repo \- repo diff - manual page for repo diff
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,diff \/\fR[\fI\,\/\fR...]
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | Show changes between commit and working tree
12 | .PP
13 | The \fB\-u\fR option causes 'repo diff' to generate diff output with file paths
14 | relative to the repository root, so the output can be applied
15 | to the Unix 'patch' command.
16 | .SH OPTIONS
17 | .TP
18 | \fB\-h\fR, \fB\-\-help\fR
19 | show this help message and exit
20 | .TP
21 | \fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
22 | number of jobs to run in parallel (default: based on
23 | number of CPU cores)
24 | .TP
25 | \fB\-u\fR, \fB\-\-absolute\fR
26 | paths are relative to the repository root
27 | .SS Logging options:
28 | .TP
29 | \fB\-v\fR, \fB\-\-verbose\fR
30 | show all output
31 | .TP
32 | \fB\-q\fR, \fB\-\-quiet\fR
33 | only show errors
34 | .SS Multi\-manifest options:
35 | .TP
36 | \fB\-\-outer\-manifest\fR
37 | operate starting at the outermost manifest
38 | .TP
39 | \fB\-\-no\-outer\-manifest\fR
40 | do not operate on outer manifests
41 | .TP
42 | \fB\-\-this\-manifest\-only\fR
43 | only operate on this (sub)manifest
44 | .TP
45 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
46 | operate on this manifest and its submanifests
47 | .PP
48 | Run `repo help diff` to view the detailed manual.
49 |
--------------------------------------------------------------------------------
/man/repo-abandon.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "July 2022" "repo abandon" "Repo Manual"
3 | .SH NAME
4 | repo \- repo abandon - manual page for repo abandon
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,abandon \/\fR[\fI\,--all | \/\fR] [\fI\,\/\fR...]
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | Permanently abandon a development branch
12 | .PP
13 | This subcommand permanently abandons a development branch by
14 | deleting it (and all its history) from your local repository.
15 | .PP
16 | It is equivalent to "git branch \fB\-D\fR ".
17 | .SH OPTIONS
18 | .TP
19 | \fB\-h\fR, \fB\-\-help\fR
20 | show this help message and exit
21 | .TP
22 | \fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
23 | number of jobs to run in parallel (default: based on
24 | number of CPU cores)
25 | .TP
26 | \fB\-\-all\fR
27 | delete all branches in all projects
28 | .SS Logging options:
29 | .TP
30 | \fB\-v\fR, \fB\-\-verbose\fR
31 | show all output
32 | .TP
33 | \fB\-q\fR, \fB\-\-quiet\fR
34 | only show errors
35 | .SS Multi\-manifest options:
36 | .TP
37 | \fB\-\-outer\-manifest\fR
38 | operate starting at the outermost manifest
39 | .TP
40 | \fB\-\-no\-outer\-manifest\fR
41 | do not operate on outer manifests
42 | .TP
43 | \fB\-\-this\-manifest\-only\fR
44 | only operate on this (sub)manifest
45 | .TP
46 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
47 | operate on this manifest and its submanifests
48 | .PP
49 | Run `repo help abandon` to view the detailed manual.
50 |
--------------------------------------------------------------------------------
/man/repo-info.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "July 2022" "repo info" "Repo Manual"
3 | .SH NAME
4 | repo \- repo info - manual page for repo info
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,info \/\fR[\fI\,-dl\/\fR] [\fI\,-o \/\fR[\fI\,-c\/\fR]] [\fI\,\/\fR...]
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | Get info on the manifest branch, current branch or unmerged branches
12 | .SH OPTIONS
13 | .TP
14 | \fB\-h\fR, \fB\-\-help\fR
15 | show this help message and exit
16 | .TP
17 | \fB\-d\fR, \fB\-\-diff\fR
18 | show full info and commit diff including remote
19 | branches
20 | .TP
21 | \fB\-o\fR, \fB\-\-overview\fR
22 | show overview of all local commits
23 | .TP
24 | \fB\-c\fR, \fB\-\-current\-branch\fR
25 | consider only checked out branches
26 | .TP
27 | \fB\-\-no\-current\-branch\fR
28 | consider all local branches
29 | .TP
30 | \fB\-l\fR, \fB\-\-local\-only\fR
31 | disable all remote operations
32 | .SS Logging options:
33 | .TP
34 | \fB\-v\fR, \fB\-\-verbose\fR
35 | show all output
36 | .TP
37 | \fB\-q\fR, \fB\-\-quiet\fR
38 | only show errors
39 | .SS Multi\-manifest options:
40 | .TP
41 | \fB\-\-outer\-manifest\fR
42 | operate starting at the outermost manifest
43 | .TP
44 | \fB\-\-no\-outer\-manifest\fR
45 | do not operate on outer manifests
46 | .TP
47 | \fB\-\-this\-manifest\-only\fR
48 | only operate on this (sub)manifest
49 | .TP
50 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
51 | operate on this manifest and its submanifests
52 | .PP
53 | Run `repo help info` to view the detailed manual.
54 |
--------------------------------------------------------------------------------
/hooks/pre-auto-gc:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # DO NOT EDIT THIS FILE
3 | # All updates should be sent upstream: https://github.com/git/git
4 | # This is synced from commit: 00e10ef10e161a913893b8cb33aa080d4ca5baa6
5 | # DO NOT EDIT THIS FILE
6 | #
7 | # An example hook script to verify if you are on battery, in case you
8 | # are running Linux or OS X. Called by git-gc --auto with no arguments.
9 | # The hook should exit with non-zero status after issuing an appropriate
10 | # message if it wants to stop the auto repacking.
11 | #
12 | # This hook is stored in the contrib/hooks directory. Your distribution
13 | # may have put this somewhere else. If you want to use this hook, you
14 | # should make this script executable then link to it in the repository
15 | # you would like to use it in.
16 | #
17 | # For example, if the hook is stored in
18 | # /usr/share/git-core/contrib/hooks/pre-auto-gc-battery:
19 | #
20 | # cd /path/to/your/repository.git
21 | # ln -sf /usr/share/git-core/contrib/hooks/pre-auto-gc-battery \
22 | # hooks/pre-auto-gc
23 |
24 | if test -x /sbin/on_ac_power && (/sbin/on_ac_power;test $? -ne 1)
25 | then
26 | exit 0
27 | elif test "$(cat /sys/class/power_supply/AC/online 2>/dev/null)" = 1
28 | then
29 | exit 0
30 | elif grep -q 'on-line' /proc/acpi/ac_adapter/AC/state 2>/dev/null
31 | then
32 | exit 0
33 | elif grep -q '0x01$' /proc/apm 2>/dev/null
34 | then
35 | exit 0
36 | elif grep -q "AC Power \+: 1" /proc/pmu/info 2>/dev/null
37 | then
38 | exit 0
39 | elif test -x /usr/bin/pmset && /usr/bin/pmset -g batt |
40 | grep -q "drawing from 'AC Power'"
41 | then
42 | exit 0
43 | fi
44 |
45 | echo "Auto packing deferred; not on AC"
46 | exit 1
47 |
--------------------------------------------------------------------------------
/man/repo-start.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "July 2022" "repo start" "Repo Manual"
3 | .SH NAME
4 | repo \- repo start - manual page for repo start
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,start \/\fR[\fI\,--all | \/\fR...]
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | Start a new branch for development
12 | .SH OPTIONS
13 | .TP
14 | \fB\-h\fR, \fB\-\-help\fR
15 | show this help message and exit
16 | .TP
17 | \fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
18 | number of jobs to run in parallel (default: based on
19 | number of CPU cores)
20 | .TP
21 | \fB\-\-all\fR
22 | begin branch in all projects
23 | .TP
24 | \fB\-r\fR REVISION, \fB\-\-rev\fR=\fI\,REVISION\/\fR, \fB\-\-revision\fR=\fI\,REVISION\/\fR
25 | point branch at this revision instead of upstream
26 | .TP
27 | \fB\-\-head\fR, \fB\-\-HEAD\fR
28 | abbreviation for \fB\-\-rev\fR HEAD
29 | .SS Logging options:
30 | .TP
31 | \fB\-v\fR, \fB\-\-verbose\fR
32 | show all output
33 | .TP
34 | \fB\-q\fR, \fB\-\-quiet\fR
35 | only show errors
36 | .SS Multi\-manifest options:
37 | .TP
38 | \fB\-\-outer\-manifest\fR
39 | operate starting at the outermost manifest
40 | .TP
41 | \fB\-\-no\-outer\-manifest\fR
42 | do not operate on outer manifests
43 | .TP
44 | \fB\-\-this\-manifest\-only\fR
45 | only operate on this (sub)manifest
46 | .TP
47 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
48 | operate on this manifest and its submanifests
49 | .PP
50 | Run `repo help start` to view the detailed manual.
51 | .SH DETAILS
52 | .PP
53 | \&'repo start' begins a new branch of development, starting from the revision
54 | specified in the manifest.
55 |
--------------------------------------------------------------------------------
/man/repo-overview.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "July 2022" "repo overview" "Repo Manual"
3 | .SH NAME
4 | repo \- repo overview - manual page for repo overview
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,overview \/\fR[\fI\,--current-branch\/\fR] [\fI\,\/\fR...]
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | Display overview of unmerged project branches
12 | .SH OPTIONS
13 | .TP
14 | \fB\-h\fR, \fB\-\-help\fR
15 | show this help message and exit
16 | .TP
17 | \fB\-c\fR, \fB\-\-current\-branch\fR
18 | consider only checked out branches
19 | .TP
20 | \fB\-\-no\-current\-branch\fR
21 | consider all local branches
22 | .SS Logging options:
23 | .TP
24 | \fB\-v\fR, \fB\-\-verbose\fR
25 | show all output
26 | .TP
27 | \fB\-q\fR, \fB\-\-quiet\fR
28 | only show errors
29 | .SS Multi\-manifest options:
30 | .TP
31 | \fB\-\-outer\-manifest\fR
32 | operate starting at the outermost manifest
33 | .TP
34 | \fB\-\-no\-outer\-manifest\fR
35 | do not operate on outer manifests
36 | .TP
37 | \fB\-\-this\-manifest\-only\fR
38 | only operate on this (sub)manifest
39 | .TP
40 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
41 | operate on this manifest and its submanifests
42 | .PP
43 | Run `repo help overview` to view the detailed manual.
44 | .SH DETAILS
45 | .PP
46 | The 'repo overview' command is used to display an overview of the projects
47 | branches, and list any local commits that have not yet been merged into the
48 | project.
49 | .PP
50 | The \fB\-c\fR/\-\-current\-branch option can be used to restrict the output to only
51 | branches currently checked out in each project. By default, all branches are
52 | displayed.
53 |
--------------------------------------------------------------------------------
/run_tests.vpython3.8:
--------------------------------------------------------------------------------
1 | # This is a vpython "spec" file.
2 | #
3 | # Read more about `vpython` and how to modify this file here:
4 | # https://chromium.googlesource.com/infra/infra/+/main/doc/users/vpython.md
5 | # List of available wheels:
6 | # https://chromium.googlesource.com/infra/infra/+/main/infra/tools/dockerbuild/wheels.md
7 |
8 | python_version: "3.8"
9 |
10 | wheel: <
11 | name: "infra/python/wheels/pytest-py3"
12 | version: "version:8.3.4"
13 | >
14 |
15 | # Required by pytest==8.3.4
16 | wheel: <
17 | name: "infra/python/wheels/py-py2_py3"
18 | version: "version:1.11.0"
19 | >
20 |
21 | # Required by pytest==8.3.4
22 | wheel: <
23 | name: "infra/python/wheels/iniconfig-py3"
24 | version: "version:1.1.1"
25 | >
26 |
27 | # Required by pytest==8.3.4
28 | wheel: <
29 | name: "infra/python/wheels/packaging-py3"
30 | version: "version:23.0"
31 | >
32 |
33 | # Required by pytest==8.3.4
34 | wheel: <
35 | name: "infra/python/wheels/pluggy-py3"
36 | version: "version:1.5.0"
37 | >
38 |
39 | # Required by pytest==8.3.4
40 | wheel: <
41 | name: "infra/python/wheels/toml-py3"
42 | version: "version:0.10.1"
43 | >
44 |
45 | # Required by pytest==8.3.4
46 | wheel: <
47 | name: "infra/python/wheels/tomli-py3"
48 | version: "version:2.1.0"
49 | >
50 |
51 | # Required by pytest==8.3.4
52 | wheel: <
53 | name: "infra/python/wheels/pyparsing-py3"
54 | version: "version:3.0.7"
55 | >
56 |
57 | # Required by pytest==8.3.4
58 | wheel: <
59 | name: "infra/python/wheels/attrs-py2_py3"
60 | version: "version:21.4.0"
61 | >
62 |
63 | # Required by pytest==8.3.4
64 | wheel: <
65 | name: "infra/python/wheels/exceptiongroup-py3"
66 | version: "version:1.1.2"
67 | >
68 |
--------------------------------------------------------------------------------
/tests/test_subcmds_init.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2020 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Unittests for the subcmds/init.py module."""
16 |
17 | import unittest
18 |
19 | from subcmds import init
20 |
21 |
22 | class InitCommand(unittest.TestCase):
23 | """Check registered all_commands."""
24 |
25 | def setUp(self):
26 | self.cmd = init.Init()
27 |
28 | def test_cli_parser_good(self):
29 | """Check valid command line options."""
30 | ARGV = ([],)
31 | for argv in ARGV:
32 | opts, args = self.cmd.OptionParser.parse_args(argv)
33 | self.cmd.ValidateOptions(opts, args)
34 |
35 | def test_cli_parser_bad(self):
36 | """Check invalid command line options."""
37 | ARGV = (
38 | # Too many arguments.
39 | ["url", "asdf"],
40 | # Conflicting options.
41 | ["--mirror", "--archive"],
42 | )
43 | for argv in ARGV:
44 | opts, args = self.cmd.OptionParser.parse_args(argv)
45 | with self.assertRaises(SystemExit):
46 | self.cmd.ValidateOptions(opts, args)
47 |
--------------------------------------------------------------------------------
/subcmds/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2008 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import os
16 |
17 |
18 | # A mapping of the subcommand name to the class that implements it.
19 | all_commands = {}
20 | all_modules = []
21 |
22 | my_dir = os.path.dirname(__file__)
23 | for py in os.listdir(my_dir):
24 | if py == "__init__.py":
25 | continue
26 |
27 | if py.endswith(".py"):
28 | name = py[:-3]
29 |
30 | clsn = name.capitalize()
31 | while clsn.find("_") > 0:
32 | h = clsn.index("_")
33 | clsn = clsn[0:h] + clsn[h + 1 :].capitalize()
34 |
35 | mod = __import__(__name__, globals(), locals(), ["%s" % name])
36 | mod = getattr(mod, name)
37 | try:
38 | cmd = getattr(mod, clsn)
39 | except AttributeError:
40 | raise SyntaxError(f"{__name__}/{py} does not define class {clsn}")
41 |
42 | name = name.replace("_", "-")
43 | cmd.NAME = name
44 | all_commands[name] = cmd
45 | all_modules.append(mod)
46 |
47 | # Add 'branch' as an alias for 'branches'.
48 | all_commands["branch"] = all_commands["branches"]
49 |
--------------------------------------------------------------------------------
/man/repo-download.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "July 2022" "repo download" "Repo Manual"
3 | .SH NAME
4 | repo \- repo download - manual page for repo download
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,download {\/\fR[\fI\,project\/\fR] \fI\,change\/\fR[\fI\,/patchset\/\fR]\fI\,}\/\fR...
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | Download and checkout a change
12 | .SH OPTIONS
13 | .TP
14 | \fB\-h\fR, \fB\-\-help\fR
15 | show this help message and exit
16 | .TP
17 | \fB\-b\fR BRANCH, \fB\-\-branch\fR=\fI\,BRANCH\/\fR
18 | create a new branch first
19 | .TP
20 | \fB\-c\fR, \fB\-\-cherry\-pick\fR
21 | cherry\-pick instead of checkout
22 | .TP
23 | \fB\-x\fR, \fB\-\-record\-origin\fR
24 | pass \fB\-x\fR when cherry\-picking
25 | .TP
26 | \fB\-r\fR, \fB\-\-revert\fR
27 | revert instead of checkout
28 | .TP
29 | \fB\-f\fR, \fB\-\-ff\-only\fR
30 | force fast\-forward merge
31 | .SS Logging options:
32 | .TP
33 | \fB\-v\fR, \fB\-\-verbose\fR
34 | show all output
35 | .TP
36 | \fB\-q\fR, \fB\-\-quiet\fR
37 | only show errors
38 | .SS Multi\-manifest options:
39 | .TP
40 | \fB\-\-outer\-manifest\fR
41 | operate starting at the outermost manifest
42 | .TP
43 | \fB\-\-no\-outer\-manifest\fR
44 | do not operate on outer manifests
45 | .TP
46 | \fB\-\-this\-manifest\-only\fR
47 | only operate on this (sub)manifest
48 | .TP
49 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
50 | operate on this manifest and its submanifests
51 | .PP
52 | Run `repo help download` to view the detailed manual.
53 | .SH DETAILS
54 | .PP
55 | The 'repo download' command downloads a change from the review system and makes
56 | it available in your project's local working directory. If no project is
57 | specified try to use current directory as a project.
58 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | # https://tox.readthedocs.io/
16 |
17 | [tox]
18 | envlist = lint, py36, py37, py38, py39, py310, py311, py312
19 | requires = virtualenv<20.22.0
20 |
21 | [gh-actions]
22 | python =
23 | 3.6: py36
24 | 3.7: py37
25 | 3.8: py38
26 | 3.9: py39
27 | 3.10: py310
28 | 3.11: py311
29 | 3.12: py312
30 |
31 | [testenv]
32 | deps =
33 | -c constraints.txt
34 | black
35 | flake8
36 | isort
37 | pytest
38 | pytest-timeout
39 | commands = {envpython} run_tests {posargs}
40 | setenv =
41 | GIT_AUTHOR_NAME = Repo test author
42 | GIT_COMMITTER_NAME = Repo test committer
43 | EMAIL = repo@gerrit.nodomain
44 |
45 | [testenv:lint]
46 | skip_install = true
47 | deps =
48 | -c constraints.txt
49 | black
50 | flake8
51 | commands =
52 | black --check {posargs:. repo run_tests release/update-hooks release/update-manpages}
53 | flake8
54 |
55 | [testenv:format]
56 | skip_install = true
57 | deps =
58 | -c constraints.txt
59 | black
60 | flake8
61 | commands =
62 | black {posargs:. repo run_tests release/update-hooks release/update-manpages}
63 | flake8
64 |
--------------------------------------------------------------------------------
/tests/test_editor.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Unittests for the editor.py module."""
16 |
17 | import unittest
18 |
19 | from editor import Editor
20 |
21 |
22 | class EditorTestCase(unittest.TestCase):
23 | """Take care of resetting Editor state across tests."""
24 |
25 | def setUp(self):
26 | self.setEditor(None)
27 |
28 | def tearDown(self):
29 | self.setEditor(None)
30 |
31 | @staticmethod
32 | def setEditor(editor):
33 | Editor._editor = editor
34 |
35 |
36 | class GetEditor(EditorTestCase):
37 | """Check GetEditor behavior."""
38 |
39 | def test_basic(self):
40 | """Basic checking of _GetEditor."""
41 | self.setEditor(":")
42 | self.assertEqual(":", Editor._GetEditor())
43 |
44 |
45 | class EditString(EditorTestCase):
46 | """Check EditString behavior."""
47 |
48 | def test_no_editor(self):
49 | """Check behavior when no editor is available."""
50 | self.setEditor(":")
51 | self.assertEqual("foo", Editor.EditString("foo"))
52 |
53 | def test_cat_editor(self):
54 | """Check behavior when editor is `cat`."""
55 | self.setEditor("cat")
56 | self.assertEqual("foo", Editor.EditString("foo"))
57 |
--------------------------------------------------------------------------------
/man/repo-wipe.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "November 2025" "repo wipe" "Repo Manual"
3 | .SH NAME
4 | repo \- repo wipe - manual page for repo wipe
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,wipe \/\fR...
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | Wipe projects from the worktree
12 | .SH OPTIONS
13 | .TP
14 | \fB\-h\fR, \fB\-\-help\fR
15 | show this help message and exit
16 | .TP
17 | \fB\-f\fR, \fB\-\-force\fR
18 | force wipe shared projects and uncommitted changes
19 | .TP
20 | \fB\-\-force\-uncommitted\fR
21 | force wipe even if there are uncommitted changes
22 | .TP
23 | \fB\-\-force\-shared\fR
24 | force wipe even if the project shares an object
25 | directory
26 | .SS Logging options:
27 | .TP
28 | \fB\-v\fR, \fB\-\-verbose\fR
29 | show all output
30 | .TP
31 | \fB\-q\fR, \fB\-\-quiet\fR
32 | only show errors
33 | .SS Multi\-manifest options:
34 | .TP
35 | \fB\-\-outer\-manifest\fR
36 | operate starting at the outermost manifest
37 | .TP
38 | \fB\-\-no\-outer\-manifest\fR
39 | do not operate on outer manifests
40 | .TP
41 | \fB\-\-this\-manifest\-only\fR
42 | only operate on this (sub)manifest
43 | .TP
44 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
45 | operate on this manifest and its submanifests
46 | .PP
47 | Run `repo help wipe` to view the detailed manual.
48 | .SH DETAILS
49 | .PP
50 | The 'repo wipe' command removes the specified projects from the worktree (the
51 | checked out source code) and deletes the project's git data from `.repo`.
52 | .PP
53 | This is a destructive operation and cannot be undone.
54 | .PP
55 | Projects can be specified either by name, or by a relative or absolute path to
56 | the project's local directory.
57 | .SH EXAMPLES
58 | .SS # Wipe the project "platform/build" by name:
59 | $ repo wipe platform/build
60 | .SS # Wipe the project at the path "build/make":
61 | $ repo wipe build/make
62 |
--------------------------------------------------------------------------------
/git_trace2_event_log.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2020 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Event logging in the git trace2 EVENT format."""
16 |
17 | from git_command import GetEventTargetPath
18 | from git_command import RepoSourceVersion
19 | from git_trace2_event_log_base import BaseEventLog
20 |
21 |
22 | class EventLog(BaseEventLog):
23 | """Event log that records events that occurred during a repo invocation.
24 |
25 | Events are written to the log as a consecutive JSON entries, one per line.
26 | Entries follow the git trace2 EVENT format.
27 |
28 | Each entry contains the following common keys:
29 | - event: The event name
30 | - sid: session-id - Unique string to allow process instance to be
31 | identified.
32 | - thread: The thread name.
33 | - time: is the UTC time of the event.
34 |
35 | Valid 'event' names and event specific fields are documented here:
36 | https://git-scm.com/docs/api-trace2#_event_format
37 | """
38 |
39 | def __init__(self, **kwargs):
40 | super().__init__(repo_source_version=RepoSourceVersion(), **kwargs)
41 |
42 | def Write(self, path=None, **kwargs):
43 | if path is None:
44 | path = self._GetEventTargetPath()
45 | return super().Write(path=path, **kwargs)
46 |
47 | def _GetEventTargetPath(self):
48 | return GetEventTargetPath()
49 |
--------------------------------------------------------------------------------
/tests/test_platform_utils.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2021 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Unittests for the platform_utils.py module."""
16 |
17 | import os
18 | import tempfile
19 | import unittest
20 |
21 | import platform_utils
22 |
23 |
24 | class RemoveTests(unittest.TestCase):
25 | """Check remove() helper."""
26 |
27 | def testMissingOk(self):
28 | """Check missing_ok handling."""
29 | with tempfile.TemporaryDirectory() as tmpdir:
30 | path = os.path.join(tmpdir, "test")
31 |
32 | # Should not fail.
33 | platform_utils.remove(path, missing_ok=True)
34 |
35 | # Should fail.
36 | self.assertRaises(OSError, platform_utils.remove, path)
37 | self.assertRaises(
38 | OSError, platform_utils.remove, path, missing_ok=False
39 | )
40 |
41 | # Should not fail if it exists.
42 | open(path, "w").close()
43 | platform_utils.remove(path, missing_ok=True)
44 | self.assertFalse(os.path.exists(path))
45 |
46 | open(path, "w").close()
47 | platform_utils.remove(path)
48 | self.assertFalse(os.path.exists(path))
49 |
50 | open(path, "w").close()
51 | platform_utils.remove(path, missing_ok=False)
52 | self.assertFalse(os.path.exists(path))
53 |
--------------------------------------------------------------------------------
/fetch.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2021 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """This module contains functions used to fetch files from various sources."""
16 |
17 | import subprocess
18 | import sys
19 | from urllib.parse import urlparse
20 | from urllib.request import urlopen
21 |
22 | from error import RepoExitError
23 |
24 |
25 | class FetchFileError(RepoExitError):
26 | """Exit error when fetch_file fails."""
27 |
28 |
29 | def fetch_file(url, verbose=False):
30 | """Fetch a file from the specified source using the appropriate protocol.
31 |
32 | Returns:
33 | The contents of the file as bytes.
34 | """
35 | scheme = urlparse(url).scheme
36 | if scheme == "gs":
37 | cmd = ["gsutil", "cat", url]
38 | errors = []
39 | try:
40 | result = subprocess.run(
41 | cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True
42 | )
43 | if result.stderr and verbose:
44 | print(
45 | 'warning: non-fatal error running "gsutil": %s'
46 | % result.stderr,
47 | file=sys.stderr,
48 | )
49 | return result.stdout
50 | except subprocess.CalledProcessError as e:
51 | errors.append(e)
52 | print(
53 | 'fatal: error running "gsutil": %s' % e.stderr, file=sys.stderr
54 | )
55 | raise FetchFileError(aggregate_errors=errors)
56 | with urlopen(url) as f:
57 | return f.read()
58 |
--------------------------------------------------------------------------------
/tests/test_hooks.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Unittests for the hooks.py module."""
16 |
17 | import unittest
18 |
19 | import hooks
20 |
21 |
22 | class RepoHookShebang(unittest.TestCase):
23 | """Check shebang parsing in RepoHook."""
24 |
25 | def test_no_shebang(self):
26 | """Lines w/out shebangs should be rejected."""
27 | DATA = ("", "#\n# foo\n", "# Bad shebang in script\n#!/foo\n")
28 | for data in DATA:
29 | self.assertIsNone(hooks.RepoHook._ExtractInterpFromShebang(data))
30 |
31 | def test_direct_interp(self):
32 | """Lines whose shebang points directly to the interpreter."""
33 | DATA = (
34 | ("#!/foo", "/foo"),
35 | ("#! /foo", "/foo"),
36 | ("#!/bin/foo ", "/bin/foo"),
37 | ("#! /usr/foo ", "/usr/foo"),
38 | ("#! /usr/foo -args", "/usr/foo"),
39 | )
40 | for shebang, interp in DATA:
41 | self.assertEqual(
42 | hooks.RepoHook._ExtractInterpFromShebang(shebang), interp
43 | )
44 |
45 | def test_env_interp(self):
46 | """Lines whose shebang launches through `env`."""
47 | DATA = (
48 | ("#!/usr/bin/env foo", "foo"),
49 | ("#!/bin/env foo", "foo"),
50 | ("#! /bin/env /bin/foo ", "/bin/foo"),
51 | )
52 | for shebang, interp in DATA:
53 | self.assertEqual(
54 | hooks.RepoHook._ExtractInterpFromShebang(shebang), interp
55 | )
56 |
--------------------------------------------------------------------------------
/man/repo-rebase.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "July 2022" "repo rebase" "Repo Manual"
3 | .SH NAME
4 | repo \- repo rebase - manual page for repo rebase
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,rebase {\/\fR[\fI\,\/\fR...] \fI\,| -i \/\fR...\fI\,}\/\fR
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | Rebase local branches on upstream branch
12 | .SH OPTIONS
13 | .TP
14 | \fB\-h\fR, \fB\-\-help\fR
15 | show this help message and exit
16 | .TP
17 | \fB\-\-fail\-fast\fR
18 | stop rebasing after first error is hit
19 | .TP
20 | \fB\-f\fR, \fB\-\-force\-rebase\fR
21 | pass \fB\-\-force\-rebase\fR to git rebase
22 | .TP
23 | \fB\-\-no\-ff\fR
24 | pass \fB\-\-no\-ff\fR to git rebase
25 | .TP
26 | \fB\-\-autosquash\fR
27 | pass \fB\-\-autosquash\fR to git rebase
28 | .TP
29 | \fB\-\-whitespace\fR=\fI\,WS\/\fR
30 | pass \fB\-\-whitespace\fR to git rebase
31 | .TP
32 | \fB\-\-auto\-stash\fR
33 | stash local modifications before starting
34 | .TP
35 | \fB\-m\fR, \fB\-\-onto\-manifest\fR
36 | rebase onto the manifest version instead of upstream
37 | HEAD (this helps to make sure the local tree stays
38 | consistent if you previously synced to a manifest)
39 | .SS Logging options:
40 | .TP
41 | \fB\-v\fR, \fB\-\-verbose\fR
42 | show all output
43 | .TP
44 | \fB\-q\fR, \fB\-\-quiet\fR
45 | only show errors
46 | .TP
47 | \fB\-i\fR, \fB\-\-interactive\fR
48 | interactive rebase (single project only)
49 | .SS Multi\-manifest options:
50 | .TP
51 | \fB\-\-outer\-manifest\fR
52 | operate starting at the outermost manifest
53 | .TP
54 | \fB\-\-no\-outer\-manifest\fR
55 | do not operate on outer manifests
56 | .TP
57 | \fB\-\-this\-manifest\-only\fR
58 | only operate on this (sub)manifest
59 | .TP
60 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
61 | operate on this manifest and its submanifests
62 | .PP
63 | Run `repo help rebase` to view the detailed manual.
64 | .SH DETAILS
65 | .PP
66 | \&'repo rebase' uses git rebase to move local changes in the current topic branch
67 | to the HEAD of the upstream history, useful when you have made commits in a
68 | topic branch but need to incorporate new upstream changes "underneath" them.
69 |
--------------------------------------------------------------------------------
/man/repo-branches.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "July 2022" "repo branches" "Repo Manual"
3 | .SH NAME
4 | repo \- repo branches - manual page for repo branches
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,branches \/\fR[\fI\,\/\fR...]
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | View current topic branches
12 | .PP
13 | Summarizes the currently available topic branches.
14 | .PP
15 | # Branch Display
16 | .PP
17 | The branch display output by this command is organized into four
18 | columns of information; for example:
19 | .TP
20 | *P nocolor
21 | | in repo
22 | .TP
23 | repo2
24 | |
25 | .PP
26 | The first column contains a * if the branch is the currently
27 | checked out branch in any of the specified projects, or a blank
28 | if no project has the branch checked out.
29 | .PP
30 | The second column contains either blank, p or P, depending upon
31 | the upload status of the branch.
32 | .IP
33 | (blank): branch not yet published by repo upload
34 | .IP
35 | P: all commits were published by repo upload
36 | p: only some commits were published by repo upload
37 | .PP
38 | The third column contains the branch name.
39 | .PP
40 | The fourth column (after the | separator) lists the projects that
41 | the branch appears in, or does not appear in. If no project list
42 | is shown, then the branch appears in all projects.
43 | .SH OPTIONS
44 | .TP
45 | \fB\-h\fR, \fB\-\-help\fR
46 | show this help message and exit
47 | .TP
48 | \fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
49 | number of jobs to run in parallel (default: based on
50 | number of CPU cores)
51 | .SS Logging options:
52 | .TP
53 | \fB\-v\fR, \fB\-\-verbose\fR
54 | show all output
55 | .TP
56 | \fB\-q\fR, \fB\-\-quiet\fR
57 | only show errors
58 | .SS Multi\-manifest options:
59 | .TP
60 | \fB\-\-outer\-manifest\fR
61 | operate starting at the outermost manifest
62 | .TP
63 | \fB\-\-no\-outer\-manifest\fR
64 | do not operate on outer manifests
65 | .TP
66 | \fB\-\-this\-manifest\-only\fR
67 | only operate on this (sub)manifest
68 | .TP
69 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
70 | operate on this manifest and its submanifests
71 | .PP
72 | Run `repo help branches` to view the detailed manual.
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # repo
2 |
3 | Repo is a tool built on top of Git. Repo helps manage many Git repositories,
4 | does the uploads to revision control systems, and automates parts of the
5 | development workflow. Repo is not meant to replace Git, only to make it
6 | easier to work with Git. The repo command is an executable Python script
7 | that you can put anywhere in your path.
8 |
9 | * Homepage:
10 | * Mailing list: [repo-discuss on Google Groups][repo-discuss]
11 | * Bug reports:
12 | * Source:
13 | * Overview:
14 | * Docs:
15 | * [repo Manifest Format](./docs/manifest-format.md)
16 | * [repo Hooks](./docs/repo-hooks.md)
17 | * [Contributing](./CONTRIBUTING.md)
18 | * Running Repo in [Microsoft Windows](./docs/windows.md)
19 | * GitHub mirror:
20 | * Postsubmit tests:
21 |
22 | ## Contact
23 |
24 | Please use the [repo-discuss] mailing list or [issue tracker] for questions.
25 |
26 | You can [file a new bug report][new-bug] under the "repo" component.
27 |
28 | Please do not e-mail individual developers for support.
29 | They do not have the bandwidth for it, and often times questions have already
30 | been asked on [repo-discuss] or bugs posted to the [issue tracker].
31 | So please search those sites first.
32 |
33 | ## Install
34 |
35 | Many distros include repo, so you might be able to install from there.
36 | ```sh
37 | # Debian/Ubuntu.
38 | $ sudo apt-get install repo
39 |
40 | # Gentoo.
41 | $ sudo emerge dev-vcs/repo
42 | ```
43 |
44 | You can install it manually as well as it's a single script.
45 | ```sh
46 | $ mkdir -p ~/.bin
47 | $ PATH="${HOME}/.bin:${PATH}"
48 | $ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/.bin/repo
49 | $ chmod a+rx ~/.bin/repo
50 | ```
51 |
52 |
53 | [new-bug]: https://issues.gerritcodereview.com/issues/new?component=1370071
54 | [issue tracker]: https://issues.gerritcodereview.com/issues?q=is:open%20componentid:1370071
55 | [repo-discuss]: https://groups.google.com/forum/#!forum/repo-discuss
56 |
--------------------------------------------------------------------------------
/tests/test_repo_trace.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2022 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Unittests for the repo_trace.py module."""
16 |
17 | import os
18 | import unittest
19 | from unittest import mock
20 |
21 | import repo_trace
22 |
23 |
24 | class TraceTests(unittest.TestCase):
25 | """Check Trace behavior."""
26 |
27 | def testTrace_MaxSizeEnforced(self):
28 | content = "git chicken"
29 |
30 | with repo_trace.Trace(content, first_trace=True):
31 | pass
32 | first_trace_size = os.path.getsize(repo_trace._TRACE_FILE)
33 |
34 | with repo_trace.Trace(content):
35 | pass
36 | self.assertGreater(
37 | os.path.getsize(repo_trace._TRACE_FILE), first_trace_size
38 | )
39 |
40 | # Check we clear everything is the last chunk is larger than _MAX_SIZE.
41 | with mock.patch("repo_trace._MAX_SIZE", 0):
42 | with repo_trace.Trace(content, first_trace=True):
43 | pass
44 | self.assertEqual(
45 | first_trace_size, os.path.getsize(repo_trace._TRACE_FILE)
46 | )
47 |
48 | # Check we only clear the chunks we need to.
49 | repo_trace._MAX_SIZE = (first_trace_size + 1) / (1024 * 1024)
50 | with repo_trace.Trace(content, first_trace=True):
51 | pass
52 | self.assertEqual(
53 | first_trace_size * 2, os.path.getsize(repo_trace._TRACE_FILE)
54 | )
55 |
56 | with repo_trace.Trace(content, first_trace=True):
57 | pass
58 | self.assertEqual(
59 | first_trace_size * 2, os.path.getsize(repo_trace._TRACE_FILE)
60 | )
61 |
--------------------------------------------------------------------------------
/man/repo-list.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "July 2022" "repo list" "Repo Manual"
3 | .SH NAME
4 | repo \- repo list - manual page for repo list
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,list \/\fR[\fI\,-f\/\fR] [\fI\,\/\fR...]
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | List projects and their associated directories
12 | .PP
13 | repo list [\-f] \fB\-r\fR str1 [str2]...
14 | .SH OPTIONS
15 | .TP
16 | \fB\-h\fR, \fB\-\-help\fR
17 | show this help message and exit
18 | .TP
19 | \fB\-r\fR, \fB\-\-regex\fR
20 | filter the project list based on regex or wildcard
21 | matching of strings
22 | .TP
23 | \fB\-g\fR GROUPS, \fB\-\-groups\fR=\fI\,GROUPS\/\fR
24 | filter the project list based on the groups the
25 | project is in
26 | .TP
27 | \fB\-a\fR, \fB\-\-all\fR
28 | show projects regardless of checkout state
29 | .TP
30 | \fB\-n\fR, \fB\-\-name\-only\fR
31 | display only the name of the repository
32 | .TP
33 | \fB\-p\fR, \fB\-\-path\-only\fR
34 | display only the path of the repository
35 | .TP
36 | \fB\-f\fR, \fB\-\-fullpath\fR
37 | display the full work tree path instead of the
38 | relative path
39 | .TP
40 | \fB\-\-relative\-to\fR=\fI\,PATH\/\fR
41 | display paths relative to this one (default: top of
42 | repo client checkout)
43 | .SS Logging options:
44 | .TP
45 | \fB\-v\fR, \fB\-\-verbose\fR
46 | show all output
47 | .TP
48 | \fB\-q\fR, \fB\-\-quiet\fR
49 | only show errors
50 | .SS Multi\-manifest options:
51 | .TP
52 | \fB\-\-outer\-manifest\fR
53 | operate starting at the outermost manifest
54 | .TP
55 | \fB\-\-no\-outer\-manifest\fR
56 | do not operate on outer manifests
57 | .TP
58 | \fB\-\-this\-manifest\-only\fR
59 | only operate on this (sub)manifest
60 | .TP
61 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
62 | operate on this manifest and its submanifests
63 | .PP
64 | Run `repo help list` to view the detailed manual.
65 | .SH DETAILS
66 | .PP
67 | List all projects; pass '.' to list the project for the cwd.
68 | .PP
69 | By default, only projects that currently exist in the checkout are shown. If you
70 | want to list all projects (using the specified filter settings), use the \fB\-\-all\fR
71 | option. If you want to show all projects regardless of the manifest groups, then
72 | also pass \fB\-\-groups\fR all.
73 | .PP
74 | This is similar to running: repo forall \fB\-c\fR 'echo "$REPO_PATH : $REPO_PROJECT"'.
75 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # Copyright (C) 2019 The Android Open Source Project
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | """Python packaging for repo."""
17 |
18 | import os
19 |
20 | import setuptools
21 |
22 |
23 | TOPDIR = os.path.dirname(os.path.abspath(__file__))
24 |
25 |
26 | # Rip out the first intro paragraph.
27 | with open(os.path.join(TOPDIR, "README.md")) as fp:
28 | lines = fp.read().splitlines()[2:]
29 | end = lines.index("")
30 | long_description = " ".join(lines[0:end])
31 |
32 |
33 | # https://packaging.python.org/tutorials/packaging-projects/
34 | setuptools.setup(
35 | name="repo",
36 | version="2",
37 | maintainer="Various",
38 | maintainer_email="repo-discuss@googlegroups.com",
39 | description="Repo helps manage many Git repositories",
40 | long_description=long_description,
41 | long_description_content_type="text/plain",
42 | url="https://gerrit.googlesource.com/git-repo/",
43 | project_urls={
44 | "Bug Tracker": "https://issues.gerritcodereview.com/issues?q=is:open%20componentid:1370071", # noqa: E501
45 | },
46 | # https://pypi.org/classifiers/
47 | classifiers=[
48 | "Development Status :: 6 - Mature",
49 | "Environment :: Console",
50 | "Intended Audience :: Developers",
51 | "License :: OSI Approved :: Apache Software License",
52 | "Natural Language :: English",
53 | "Operating System :: MacOS :: MacOS X",
54 | "Operating System :: Microsoft :: Windows :: Windows 10",
55 | "Operating System :: POSIX :: Linux",
56 | "Programming Language :: Python :: 3",
57 | "Programming Language :: Python :: 3 :: Only",
58 | "Topic :: Software Development :: Version Control :: Git",
59 | ],
60 | python_requires=">=3.6",
61 | packages=["subcmds"],
62 | )
63 |
--------------------------------------------------------------------------------
/requirements.json:
--------------------------------------------------------------------------------
1 | # This file declares various requirements for this version of repo. The
2 | # launcher script will load it and check the constraints before trying to run
3 | # us. This avoids issues of the launcher using an old version of Python (e.g.
4 | # 3.5) while the codebase has moved on to requiring something much newer (e.g.
5 | # 3.8). If the launcher tried to import us, it would fail with syntax errors.
6 |
7 | # This is a JSON file with line-level comments allowed.
8 |
9 | # Always keep backwards compatibility in mine. The launcher script is robust
10 | # against missing values, but when a field is renamed/removed, it means older
11 | # versions of the launcher script won't be able to enforce the constraint.
12 |
13 | # When requiring versions, always use lists as they are easy to parse & compare
14 | # in Python. Strings would require futher processing to turn into a list.
15 |
16 | # Version constraints should be expressed in pairs: soft & hard. Soft versions
17 | # are when we start warning users that their software too old and we're planning
18 | # on dropping support for it, so they need to start planning system upgrades.
19 | # Hard versions are when we refuse to work the tool. Users will be shown an
20 | # error message before we abort entirely.
21 |
22 | # When deciding whether to upgrade a version requirement, check out the distro
23 | # lists to see who will be impacted:
24 | # https://gerrit.googlesource.com/git-repo/+/HEAD/docs/release-process.md#Project-References
25 |
26 | {
27 | # The repo launcher itself. This allows us to force people to upgrade as some
28 | # ignore the warnings about it being out of date, or install ancient versions
29 | # to start with for whatever reason.
30 | #
31 | # NB: Repo launchers started checking this file with repo-2.12, so listing
32 | # versions older than that won't make a difference.
33 | "repo": {
34 | "hard": [2, 11],
35 | "soft": [2, 11]
36 | },
37 |
38 | # Supported Python versions.
39 | #
40 | # python-3.6 is in Ubuntu Bionic.
41 | # python-3.7 is in Debian Buster.
42 | "python": {
43 | "hard": [3, 6],
44 | "soft": [3, 6]
45 | },
46 |
47 | # Supported git versions.
48 | #
49 | # git-1.9.1 is in Ubuntu Trusty.
50 | # git-2.1.4 is in Debian Jessie.
51 | # git-2.7.4 is in Ubuntu Xenial.
52 | # git-2.11.0 is in Debian Stretch.
53 | # git-2.17.0 is in Ubuntu Bionic.
54 | # git-2.20.1 is in Debian Buster.
55 | "git": {
56 | "hard": [1, 9, 1],
57 | "soft": [2, 7, 4]
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tests/test_error.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2021 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Unittests for the error.py module."""
16 |
17 | import inspect
18 | import pickle
19 | import unittest
20 |
21 | import command
22 | import error
23 | import fetch
24 | import git_command
25 | import project
26 | from subcmds import all_modules
27 |
28 |
29 | imports = all_modules + [
30 | error,
31 | project,
32 | git_command,
33 | fetch,
34 | command,
35 | ]
36 |
37 |
38 | class PickleTests(unittest.TestCase):
39 | """Make sure all our custom exceptions can be pickled."""
40 |
41 | def getExceptions(self):
42 | """Return all our custom exceptions."""
43 | for entry in imports:
44 | for name in dir(entry):
45 | cls = getattr(entry, name)
46 | if isinstance(cls, type) and issubclass(cls, Exception):
47 | yield cls
48 |
49 | def testExceptionLookup(self):
50 | """Make sure our introspection logic works."""
51 | classes = list(self.getExceptions())
52 | self.assertIn(error.HookError, classes)
53 | # Don't assert the exact number to avoid being a change-detector test.
54 | self.assertGreater(len(classes), 10)
55 |
56 | def testPickle(self):
57 | """Try to pickle all the exceptions."""
58 | for cls in self.getExceptions():
59 | args = inspect.getfullargspec(cls.__init__).args[1:]
60 | obj = cls(*args)
61 | p = pickle.dumps(obj)
62 | try:
63 | newobj = pickle.loads(p)
64 | except Exception as e: # pylint: disable=broad-except
65 | self.fail(
66 | "Class %s is unable to be pickled: %s\n"
67 | "Incomplete super().__init__(...) call?" % (cls, e)
68 | )
69 | self.assertIsInstance(newobj, cls)
70 | self.assertEqual(str(obj), str(newobj))
71 |
--------------------------------------------------------------------------------
/man/repo-diffmanifests.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "July 2022" "repo diffmanifests" "Repo Manual"
3 | .SH NAME
4 | repo \- repo diffmanifests - manual page for repo diffmanifests
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,diffmanifests manifest1.xml \/\fR[\fI\,manifest2.xml\/\fR] [\fI\,options\/\fR]
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | Manifest diff utility
12 | .SH OPTIONS
13 | .TP
14 | \fB\-h\fR, \fB\-\-help\fR
15 | show this help message and exit
16 | .TP
17 | \fB\-\-raw\fR
18 | display raw diff
19 | .TP
20 | \fB\-\-no\-color\fR
21 | does not display the diff in color
22 | .TP
23 | \fB\-\-pretty\-format=\fR
24 | print the log using a custom git pretty format string
25 | .SS Logging options:
26 | .TP
27 | \fB\-v\fR, \fB\-\-verbose\fR
28 | show all output
29 | .TP
30 | \fB\-q\fR, \fB\-\-quiet\fR
31 | only show errors
32 | .SS Multi\-manifest options:
33 | .TP
34 | \fB\-\-outer\-manifest\fR
35 | operate starting at the outermost manifest
36 | .TP
37 | \fB\-\-no\-outer\-manifest\fR
38 | do not operate on outer manifests
39 | .TP
40 | \fB\-\-this\-manifest\-only\fR
41 | only operate on this (sub)manifest
42 | .TP
43 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
44 | operate on this manifest and its submanifests
45 | .PP
46 | Run `repo help diffmanifests` to view the detailed manual.
47 | .SH DETAILS
48 | .PP
49 | The repo diffmanifests command shows differences between project revisions of
50 | manifest1 and manifest2. if manifest2 is not specified, current manifest.xml
51 | will be used instead. Both absolute and relative paths may be used for
52 | manifests. Relative paths start from project's ".repo/manifests" folder.
53 | .PP
54 | The \fB\-\-raw\fR option Displays the diff in a way that facilitates parsing, the
55 | project pattern will be [] and the
56 | commit pattern will be with status values respectively :
57 | .IP
58 | A = Added project
59 | R = Removed project
60 | C = Changed project
61 | U = Project with unreachable revision(s) (revision(s) not found)
62 | .PP
63 | for project, and
64 | .IP
65 | A = Added commit
66 | R = Removed commit
67 | .PP
68 | for a commit.
69 | .PP
70 | Only changed projects may contain commits, and commit status always starts with
71 | a space, and are part of last printed project. Unreachable revisions may occur
72 | if project is not up to date or if repo has not been initialized with all the
73 | groups, in which case some projects won't be synced and their revisions won't be
74 | found.
75 |
--------------------------------------------------------------------------------
/subcmds/selfupdate.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2009 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import optparse
16 |
17 | from command import Command
18 | from command import MirrorSafeCommand
19 | from error import RepoExitError
20 | from repo_logging import RepoLogger
21 | from subcmds.sync import _PostRepoFetch
22 | from subcmds.sync import _PostRepoUpgrade
23 |
24 |
25 | logger = RepoLogger(__file__)
26 |
27 |
28 | class SelfupdateError(RepoExitError):
29 | """Exit error for failed selfupdate command."""
30 |
31 |
32 | class Selfupdate(Command, MirrorSafeCommand):
33 | COMMON = False
34 | helpSummary = "Update repo to the latest version"
35 | helpUsage = """
36 | %prog
37 | """
38 | helpDescription = """
39 | The '%prog' command upgrades repo to the latest version, if a
40 | newer version is available.
41 |
42 | Normally this is done automatically by 'repo sync' and does not
43 | need to be performed by an end-user.
44 | """
45 |
46 | def _Options(self, p):
47 | g = p.add_option_group("repo Version options")
48 | g.add_option(
49 | "--no-repo-verify",
50 | dest="repo_verify",
51 | default=True,
52 | action="store_false",
53 | help="do not verify repo source code",
54 | )
55 | g.add_option(
56 | "--repo-upgraded",
57 | action="store_true",
58 | help=optparse.SUPPRESS_HELP,
59 | )
60 |
61 | def Execute(self, opt, args):
62 | rp = self.manifest.repoProject
63 | rp.PreSync()
64 |
65 | if opt.repo_upgraded:
66 | _PostRepoUpgrade(self.manifest)
67 |
68 | else:
69 | result = rp.Sync_NetworkHalf()
70 | if result.error:
71 | logger.error("error: can't update repo")
72 | raise SelfupdateError(aggregate_errors=[result.error])
73 |
74 | rp.bare_git.gc("--auto")
75 | _PostRepoFetch(rp, repo_verify=opt.repo_verify, verbose=True)
76 |
--------------------------------------------------------------------------------
/release/util.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2020 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Random utility code for release tools."""
16 |
17 | from pathlib import Path
18 | import re
19 | import shlex
20 | import subprocess
21 | import sys
22 |
23 |
24 | assert sys.version_info >= (3, 6), "This module requires Python 3.6+"
25 |
26 |
27 | THIS_FILE = Path(__file__).resolve()
28 | TOPDIR = THIS_FILE.parent.parent
29 | HOMEDIR = Path("~").expanduser()
30 |
31 |
32 | # These are the release keys we sign with.
33 | KEYID_DSA = "8BB9AD793E8E6153AF0F9A4416530D5E920F5C65"
34 | KEYID_RSA = "A34A13BE8E76BFF46A0C022DA2E75A824AAB9624"
35 | KEYID_ECC = "E1F9040D7A3F6DAFAC897CD3D3B95DA243E48A39"
36 |
37 |
38 | def cmdstr(cmd):
39 | """Get a nicely quoted shell command."""
40 | return " ".join(shlex.quote(x) for x in cmd)
41 |
42 |
43 | def run(opts, cmd, check=True, **kwargs):
44 | """Helper around subprocess.run to include logging."""
45 | print("+", cmdstr(cmd))
46 | if opts.dryrun:
47 | cmd = ["true", "--"] + cmd
48 | try:
49 | return subprocess.run(cmd, check=check, **kwargs)
50 | except subprocess.CalledProcessError as e:
51 | print(f"aborting: {e}", file=sys.stderr)
52 | sys.exit(1)
53 |
54 |
55 | def import_release_key(opts):
56 | """Import the public key of the official release repo signing key."""
57 | # Extract the key from our repo launcher.
58 | launcher = getattr(opts, "launcher", TOPDIR / "repo")
59 | print(f'Importing keys from "{launcher}" launcher script')
60 | with open(launcher, encoding="utf-8") as fp:
61 | data = fp.read()
62 |
63 | keys = re.findall(
64 | r"\n-----BEGIN PGP PUBLIC KEY BLOCK-----\n[^-]*"
65 | r"\n-----END PGP PUBLIC KEY BLOCK-----\n",
66 | data,
67 | flags=re.M,
68 | )
69 | run(opts, ["gpg", "--import"], input="\n".join(keys).encode("utf-8"))
70 |
71 | print("Marking keys as fully trusted")
72 | run(
73 | opts,
74 | ["gpg", "--import-ownertrust"],
75 | input=f"{KEYID_DSA}:6:\n".encode("utf-8"),
76 | )
77 |
--------------------------------------------------------------------------------
/subcmds/version.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2009 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import platform
16 | import sys
17 |
18 | from command import Command
19 | from command import MirrorSafeCommand
20 | from git_command import git
21 | from git_command import RepoSourceVersion
22 | from git_command import user_agent
23 | from git_refs import HEAD
24 | from wrapper import Wrapper
25 |
26 |
27 | class Version(Command, MirrorSafeCommand):
28 | wrapper_version = None
29 | wrapper_path = None
30 |
31 | COMMON = False
32 | helpSummary = "Display the version of repo"
33 | helpUsage = """
34 | %prog
35 | """
36 |
37 | def Execute(self, opt, args):
38 | rp = self.manifest.repoProject
39 | rem = rp.GetRemote()
40 | branch = rp.GetBranch("default")
41 |
42 | # These might not be the same. Report them both.
43 | src_ver = RepoSourceVersion()
44 | rp_ver = rp.bare_git.describe(HEAD)
45 | print(f"repo version {rp_ver}")
46 | print(f" (from {rem.url})")
47 | print(f" (tracking {branch.merge})")
48 | print(f" ({rp.bare_git.log('-1', '--format=%cD', HEAD)})")
49 |
50 | if self.wrapper_path is not None:
51 | print(f"repo launcher version {self.wrapper_version}")
52 | print(f" (from {self.wrapper_path})")
53 |
54 | if src_ver != rp_ver:
55 | print(f" (currently at {src_ver})")
56 |
57 | print(f"repo User-Agent {user_agent.repo}")
58 | print(f"git {git.version_tuple().full}")
59 | print(f"git User-Agent {user_agent.git}")
60 | print(f"Python {sys.version}")
61 | uname = platform.uname()
62 | if sys.version_info.major < 3:
63 | # Python 3 returns a named tuple, but Python 2 is simpler.
64 | print(uname)
65 | else:
66 | print(f"OS {uname.system} {uname.release} ({uname.version})")
67 | processor = uname.processor if uname.processor else "unknown"
68 | print(f"CPU {uname.machine} ({processor})")
69 | print("Bug reports:", Wrapper().BUG_URL)
70 |
--------------------------------------------------------------------------------
/tests/test_subcmds_upload.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2023 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Unittests for the subcmds/upload.py module."""
16 |
17 | import unittest
18 | from unittest import mock
19 |
20 | from error import GitError
21 | from error import UploadError
22 | from subcmds import upload
23 |
24 |
25 | class UnexpectedError(Exception):
26 | """An exception not expected by upload command."""
27 |
28 |
29 | class UploadCommand(unittest.TestCase):
30 | """Check registered all_commands."""
31 |
32 | def setUp(self):
33 | self.cmd = upload.Upload()
34 | self.branch = mock.MagicMock()
35 | self.people = mock.MagicMock()
36 | self.opt, _ = self.cmd.OptionParser.parse_args([])
37 | mock.patch.object(
38 | self.cmd, "_AppendAutoList", return_value=None
39 | ).start()
40 | mock.patch.object(self.cmd, "git_event_log").start()
41 |
42 | def tearDown(self):
43 | mock.patch.stopall()
44 |
45 | def test_UploadAndReport_UploadError(self):
46 | """Check UploadExitError raised when UploadError encountered."""
47 | side_effect = UploadError("upload error")
48 | with mock.patch.object(
49 | self.cmd, "_UploadBranch", side_effect=side_effect
50 | ):
51 | with self.assertRaises(upload.UploadExitError):
52 | self.cmd._UploadAndReport(self.opt, [self.branch], self.people)
53 |
54 | def test_UploadAndReport_GitError(self):
55 | """Check UploadExitError raised when GitError encountered."""
56 | side_effect = GitError("some git error")
57 | with mock.patch.object(
58 | self.cmd, "_UploadBranch", side_effect=side_effect
59 | ):
60 | with self.assertRaises(upload.UploadExitError):
61 | self.cmd._UploadAndReport(self.opt, [self.branch], self.people)
62 |
63 | def test_UploadAndReport_UnhandledError(self):
64 | """Check UnexpectedError passed through."""
65 | side_effect = UnexpectedError("some os error")
66 | with mock.patch.object(
67 | self.cmd, "_UploadBranch", side_effect=side_effect
68 | ):
69 | with self.assertRaises(type(side_effect)):
70 | self.cmd._UploadAndReport(self.opt, [self.branch], self.people)
71 |
--------------------------------------------------------------------------------
/tests/test_color.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2024 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Unittests for the color.py module."""
16 |
17 | import os
18 | import unittest
19 |
20 | import color
21 | import git_config
22 |
23 |
24 | def fixture(*paths):
25 | """Return a path relative to test/fixtures."""
26 | return os.path.join(os.path.dirname(__file__), "fixtures", *paths)
27 |
28 |
29 | class ColoringTests(unittest.TestCase):
30 | """tests of the Coloring class."""
31 |
32 | def setUp(self):
33 | """Create a GitConfig object using the test.gitconfig fixture."""
34 | config_fixture = fixture("test.gitconfig")
35 | self.config = git_config.GitConfig(config_fixture)
36 | color.SetDefaultColoring("true")
37 | self.color = color.Coloring(self.config, "status")
38 |
39 | def test_Color_Parse_all_params_none(self):
40 | """all params are None"""
41 | val = self.color._parse(None, None, None, None)
42 | self.assertEqual("", val)
43 |
44 | def test_Color_Parse_first_parameter_none(self):
45 | """check fg & bg & attr"""
46 | val = self.color._parse(None, "black", "red", "ul")
47 | self.assertEqual("\x1b[4;30;41m", val)
48 |
49 | def test_Color_Parse_one_entry(self):
50 | """check fg"""
51 | val = self.color._parse("one", None, None, None)
52 | self.assertEqual("\033[33m", val)
53 |
54 | def test_Color_Parse_two_entry(self):
55 | """check fg & bg"""
56 | val = self.color._parse("two", None, None, None)
57 | self.assertEqual("\033[35;46m", val)
58 |
59 | def test_Color_Parse_three_entry(self):
60 | """check fg & bg & attr"""
61 | val = self.color._parse("three", None, None, None)
62 | self.assertEqual("\033[4;30;41m", val)
63 |
64 | def test_Color_Parse_reset_entry(self):
65 | """check reset entry"""
66 | val = self.color._parse("reset", None, None, None)
67 | self.assertEqual("\033[m", val)
68 |
69 | def test_Color_Parse_empty_entry(self):
70 | """check empty entry"""
71 | val = self.color._parse("none", "blue", "white", "dim")
72 | self.assertEqual("\033[2;34;47m", val)
73 | val = self.color._parse("empty", "green", "white", "bold")
74 | self.assertEqual("\033[1;32;47m", val)
75 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2022 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Common fixtures for pytests."""
16 |
17 | import pathlib
18 |
19 | import pytest
20 |
21 | import platform_utils
22 | import repo_trace
23 |
24 |
25 | @pytest.fixture(autouse=True)
26 | def disable_repo_trace(tmp_path):
27 | """Set an environment marker to relax certain strict checks for test code.""" # noqa: E501
28 | repo_trace._TRACE_FILE = str(tmp_path / "TRACE_FILE_from_test")
29 |
30 |
31 | # adapted from pytest-home 0.5.1
32 | def _set_home(monkeypatch, path: pathlib.Path):
33 | """
34 | Set the home dir using a pytest monkeypatch context.
35 | """
36 | win = platform_utils.isWindows()
37 | vars = ["HOME"] + win * ["USERPROFILE"]
38 | for var in vars:
39 | monkeypatch.setenv(var, str(path))
40 | return path
41 |
42 |
43 | # copied from
44 | # https://github.com/pytest-dev/pytest/issues/363#issuecomment-1335631998
45 | @pytest.fixture(scope="session")
46 | def monkeysession():
47 | with pytest.MonkeyPatch.context() as mp:
48 | yield mp
49 |
50 |
51 | @pytest.fixture(autouse=True, scope="session")
52 | def session_tmp_home_dir(tmp_path_factory, monkeysession):
53 | """Set HOME to a temporary directory, avoiding user's .gitconfig.
54 |
55 | b/302797407
56 |
57 | Set home at session scope to take effect prior to
58 | ``test_wrapper.GitCheckoutTestCase.setUpClass``.
59 | """
60 | return _set_home(monkeysession, tmp_path_factory.mktemp("home"))
61 |
62 |
63 | # adapted from pytest-home 0.5.1
64 | @pytest.fixture(autouse=True)
65 | def tmp_home_dir(monkeypatch, tmp_path_factory):
66 | """Set HOME to a temporary directory.
67 |
68 | Ensures that state doesn't accumulate in $HOME across tests.
69 |
70 | Note that in conjunction with session_tmp_homedir, the HOME
71 | dir is patched twice, once at session scope, and then again at
72 | the function scope.
73 | """
74 | return _set_home(monkeypatch, tmp_path_factory.mktemp("home"))
75 |
76 |
77 | @pytest.fixture(autouse=True)
78 | def setup_user_identity(monkeysession, scope="session"):
79 | """Set env variables for author and committer name and email."""
80 | monkeysession.setenv("GIT_AUTHOR_NAME", "Foo Bar")
81 | monkeysession.setenv("GIT_COMMITTER_NAME", "Foo Bar")
82 | monkeysession.setenv("GIT_AUTHOR_EMAIL", "foo@bar.baz")
83 | monkeysession.setenv("GIT_COMMITTER_EMAIL", "foo@bar.baz")
84 |
--------------------------------------------------------------------------------
/run_tests.vpython3:
--------------------------------------------------------------------------------
1 | # This is a vpython "spec" file.
2 | #
3 | # Read more about `vpython` and how to modify this file here:
4 | # https://chromium.googlesource.com/infra/infra/+/main/doc/users/vpython.md
5 | # List of available wheels:
6 | # https://chromium.googlesource.com/infra/infra/+/main/infra/tools/dockerbuild/wheels.md
7 |
8 | python_version: "3.11"
9 |
10 | wheel: <
11 | name: "infra/python/wheels/pytest-py3"
12 | version: "version:8.3.4"
13 | >
14 |
15 | # Required by pytest==8.3.4
16 | wheel: <
17 | name: "infra/python/wheels/py-py2_py3"
18 | version: "version:1.11.0"
19 | >
20 |
21 | # Required by pytest==8.3.4
22 | wheel: <
23 | name: "infra/python/wheels/iniconfig-py3"
24 | version: "version:1.1.1"
25 | >
26 |
27 | # Required by pytest==8.3.4
28 | wheel: <
29 | name: "infra/python/wheels/packaging-py3"
30 | version: "version:23.0"
31 | >
32 |
33 | # Required by pytest==8.3.4
34 | wheel: <
35 | name: "infra/python/wheels/pluggy-py3"
36 | version: "version:1.5.0"
37 | >
38 |
39 | # Required by pytest==8.3.4
40 | wheel: <
41 | name: "infra/python/wheels/toml-py3"
42 | version: "version:0.10.1"
43 | >
44 |
45 | # Required by pytest==8.3.4
46 | wheel: <
47 | name: "infra/python/wheels/pyparsing-py3"
48 | version: "version:3.0.7"
49 | >
50 |
51 | # Required by pytest==8.3.4
52 | wheel: <
53 | name: "infra/python/wheels/attrs-py2_py3"
54 | version: "version:21.4.0"
55 | >
56 |
57 | # NB: Keep in sync with constraints.txt.
58 | wheel: <
59 | name: "infra/python/wheels/black-py3"
60 | version: "version:25.1.0"
61 | >
62 |
63 | # Required by black==25.1.0
64 | wheel: <
65 | name: "infra/python/wheels/mypy-extensions-py3"
66 | version: "version:0.4.3"
67 | >
68 |
69 | # Required by black==25.1.0
70 | wheel: <
71 | name: "infra/python/wheels/tomli-py3"
72 | version: "version:2.0.1"
73 | >
74 |
75 | # Required by black==25.1.0
76 | wheel: <
77 | name: "infra/python/wheels/platformdirs-py3"
78 | version: "version:2.5.2"
79 | >
80 |
81 | # Required by black==25.1.0
82 | wheel: <
83 | name: "infra/python/wheels/pathspec-py3"
84 | version: "version:0.9.0"
85 | >
86 |
87 | # Required by black==25.1.0
88 | wheel: <
89 | name: "infra/python/wheels/typing-extensions-py3"
90 | version: "version:4.3.0"
91 | >
92 |
93 | # Required by black==25.1.0
94 | wheel: <
95 | name: "infra/python/wheels/click-py3"
96 | version: "version:8.0.3"
97 | >
98 |
99 | wheel: <
100 | name: "infra/python/wheels/flake8-py2_py3"
101 | version: "version:6.0.0"
102 | >
103 |
104 | # Required by flake8==6.0.0
105 | wheel: <
106 | name: "infra/python/wheels/mccabe-py2_py3"
107 | version: "version:0.7.0"
108 | >
109 |
110 | # Required by flake8==6.0.0
111 | wheel: <
112 | name: "infra/python/wheels/pyflakes-py2_py3"
113 | version: "version:3.0.1"
114 | >
115 |
116 | # Required by flake8==6.0.0
117 | wheel: <
118 | name: "infra/python/wheels/pycodestyle-py2_py3"
119 | version: "version:2.10.0"
120 | >
121 |
122 | wheel: <
123 | name: "infra/python/wheels/isort-py3"
124 | version: "version:5.10.1"
125 | >
126 |
--------------------------------------------------------------------------------
/subcmds/diff.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2008 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import functools
16 | import io
17 |
18 | from command import DEFAULT_LOCAL_JOBS
19 | from command import PagedCommand
20 |
21 |
22 | class Diff(PagedCommand):
23 | COMMON = True
24 | helpSummary = "Show changes between commit and working tree"
25 | helpUsage = """
26 | %prog [...]
27 |
28 | The -u option causes '%prog' to generate diff output with file paths
29 | relative to the repository root, so the output can be applied
30 | to the Unix 'patch' command.
31 | """
32 | PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
33 |
34 | def _Options(self, p):
35 | p.add_option(
36 | "-u",
37 | "--absolute",
38 | action="store_true",
39 | help="paths are relative to the repository root",
40 | )
41 |
42 | @classmethod
43 | def _ExecuteOne(cls, absolute, local, project_idx):
44 | """Obtains the diff for a specific project.
45 |
46 | Args:
47 | absolute: Paths are relative to the root.
48 | local: a boolean, if True, the path is relative to the local
49 | (sub)manifest. If false, the path is relative to the outermost
50 | manifest.
51 | project_idx: Project index to get status of.
52 |
53 | Returns:
54 | The status of the project.
55 | """
56 | buf = io.StringIO()
57 | project = cls.get_parallel_context()["projects"][project_idx]
58 | ret = project.PrintWorkTreeDiff(absolute, output_redir=buf, local=local)
59 | return (ret, buf.getvalue())
60 |
61 | def Execute(self, opt, args):
62 | all_projects = self.GetProjects(
63 | args, all_manifests=not opt.this_manifest_only
64 | )
65 |
66 | def _ProcessResults(_pool, _output, results):
67 | ret = 0
68 | for state, output in results:
69 | if output:
70 | print(output, end="")
71 | if not state:
72 | ret = 1
73 | return ret
74 |
75 | with self.ParallelContext():
76 | self.get_parallel_context()["projects"] = all_projects
77 | return self.ExecuteInParallel(
78 | opt.jobs,
79 | functools.partial(
80 | self._ExecuteOne, opt.absolute, opt.this_manifest_only
81 | ),
82 | range(len(all_projects)),
83 | callback=_ProcessResults,
84 | ordered=True,
85 | chunksize=1,
86 | )
87 |
--------------------------------------------------------------------------------
/repo_logging.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2023 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Logic for printing user-friendly logs in repo."""
16 |
17 | import logging
18 |
19 | from color import Coloring
20 | from error import RepoExitError
21 |
22 |
23 | SEPARATOR = "=" * 80
24 | MAX_PRINT_ERRORS = 5
25 |
26 |
27 | class _ConfigMock:
28 | """Default coloring config to use when Logging.config is not set."""
29 |
30 | def __init__(self):
31 | self.default_values = {"color.ui": "auto"}
32 |
33 | def GetString(self, x):
34 | return self.default_values.get(x, None)
35 |
36 |
37 | class _LogColoring(Coloring):
38 | """Coloring outstream for logging."""
39 |
40 | def __init__(self, config):
41 | super().__init__(config, "logs")
42 | self.error = self.nofmt_colorer("error", fg="red")
43 | self.warning = self.nofmt_colorer("warn", fg="yellow")
44 | self.levelMap = {
45 | "WARNING": self.warning,
46 | "ERROR": self.error,
47 | }
48 |
49 |
50 | class _LogColoringFormatter(logging.Formatter):
51 | """Coloring formatter for logging."""
52 |
53 | def __init__(self, config=None, *args, **kwargs):
54 | self.config = config if config else _ConfigMock()
55 | self.colorer = _LogColoring(self.config)
56 | super().__init__(*args, **kwargs)
57 |
58 | def format(self, record):
59 | """Formats |record| with color."""
60 | msg = super().format(record)
61 | colorer = self.colorer.levelMap.get(record.levelname)
62 | return msg if not colorer else colorer(msg)
63 |
64 |
65 | class RepoLogger(logging.Logger):
66 | """Repo Logging Module."""
67 |
68 | def __init__(self, name: str, config=None, **kwargs):
69 | super().__init__(name, **kwargs)
70 | handler = logging.StreamHandler()
71 | handler.setFormatter(_LogColoringFormatter(config))
72 | self.addHandler(handler)
73 |
74 | def log_aggregated_errors(self, err: RepoExitError):
75 | """Print all aggregated logs."""
76 | self.error(SEPARATOR)
77 |
78 | if not err.aggregate_errors:
79 | self.error("Repo command failed: %s", type(err).__name__)
80 | self.error("\t%s", str(err))
81 | return
82 |
83 | self.error(
84 | "Repo command failed due to the following `%s` errors:",
85 | type(err).__name__,
86 | )
87 | self.error(
88 | "\n".join(str(e) for e in err.aggregate_errors[:MAX_PRINT_ERRORS])
89 | )
90 |
91 | diff = len(err.aggregate_errors) - MAX_PRINT_ERRORS
92 | if diff > 0:
93 | self.error("+%d additional errors...", diff)
94 |
--------------------------------------------------------------------------------
/tests/test_ssh.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Unittests for the ssh.py module."""
16 |
17 | import multiprocessing
18 | import subprocess
19 | import unittest
20 | from unittest import mock
21 |
22 | import ssh
23 |
24 |
25 | class SshTests(unittest.TestCase):
26 | """Tests the ssh functions."""
27 |
28 | def test_parse_ssh_version(self):
29 | """Check _parse_ssh_version() handling."""
30 | ver = ssh._parse_ssh_version("Unknown\n")
31 | self.assertEqual(ver, ())
32 | ver = ssh._parse_ssh_version("OpenSSH_1.0\n")
33 | self.assertEqual(ver, (1, 0))
34 | ver = ssh._parse_ssh_version(
35 | "OpenSSH_6.6.1p1 Ubuntu-2ubuntu2.13, OpenSSL 1.0.1f 6 Jan 2014\n"
36 | )
37 | self.assertEqual(ver, (6, 6, 1))
38 | ver = ssh._parse_ssh_version(
39 | "OpenSSH_7.6p1 Ubuntu-4ubuntu0.3, OpenSSL 1.0.2n 7 Dec 2017\n"
40 | )
41 | self.assertEqual(ver, (7, 6))
42 | ver = ssh._parse_ssh_version("OpenSSH_9.0p1, LibreSSL 3.3.6\n")
43 | self.assertEqual(ver, (9, 0))
44 |
45 | def test_version(self):
46 | """Check version() handling."""
47 | with mock.patch("ssh._run_ssh_version", return_value="OpenSSH_1.2\n"):
48 | self.assertEqual(ssh.version(), (1, 2))
49 |
50 | def test_context_manager_empty(self):
51 | """Verify context manager with no clients works correctly."""
52 | with multiprocessing.Manager() as manager:
53 | with ssh.ProxyManager(manager):
54 | pass
55 |
56 | def test_context_manager_child_cleanup(self):
57 | """Verify orphaned clients & masters get cleaned up."""
58 | with multiprocessing.Manager() as manager:
59 | with ssh.ProxyManager(manager) as ssh_proxy:
60 | client = subprocess.Popen(["sleep", "964853320"])
61 | ssh_proxy.add_client(client)
62 | master = subprocess.Popen(["sleep", "964853321"])
63 | ssh_proxy.add_master(master)
64 | # If the process still exists, these will throw timeout errors.
65 | client.wait(0)
66 | master.wait(0)
67 |
68 | def test_ssh_sock(self):
69 | """Check sock() function."""
70 | manager = multiprocessing.Manager()
71 | proxy = ssh.ProxyManager(manager)
72 | with mock.patch("tempfile.mkdtemp", return_value="/tmp/foo"):
73 | # Old ssh version uses port.
74 | with mock.patch("ssh.version", return_value=(6, 6)):
75 | self.assertTrue(proxy.sock().endswith("%p"))
76 |
77 | proxy._sock_path = None
78 | # New ssh version uses hash.
79 | with mock.patch("ssh.version", return_value=(6, 7)):
80 | self.assertTrue(proxy.sock().endswith("%C"))
81 |
--------------------------------------------------------------------------------
/subcmds/prune.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2008 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import itertools
16 |
17 | from color import Coloring
18 | from command import DEFAULT_LOCAL_JOBS
19 | from command import PagedCommand
20 |
21 |
22 | class Prune(PagedCommand):
23 | COMMON = True
24 | helpSummary = "Prune (delete) already merged topics"
25 | helpUsage = """
26 | %prog [...]
27 | """
28 | PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
29 |
30 | @classmethod
31 | def _ExecuteOne(cls, project_idx):
32 | """Process one project."""
33 | project = cls.get_parallel_context()["projects"][project_idx]
34 | return project.PruneHeads()
35 |
36 | def Execute(self, opt, args):
37 | projects = self.GetProjects(
38 | args, all_manifests=not opt.this_manifest_only
39 | )
40 |
41 | # NB: Should be able to refactor this module to display summary as
42 | # results come back from children.
43 | def _ProcessResults(_pool, _output, results):
44 | return list(itertools.chain.from_iterable(results))
45 |
46 | with self.ParallelContext():
47 | self.get_parallel_context()["projects"] = projects
48 | all_branches = self.ExecuteInParallel(
49 | opt.jobs,
50 | self._ExecuteOne,
51 | range(len(projects)),
52 | callback=_ProcessResults,
53 | ordered=True,
54 | )
55 |
56 | if not all_branches:
57 | return
58 |
59 | class Report(Coloring):
60 | def __init__(self, config):
61 | Coloring.__init__(self, config, "status")
62 | self.project = self.printer("header", attr="bold")
63 |
64 | out = Report(all_branches[0].project.config)
65 | out.project("Pending Branches")
66 | out.nl()
67 |
68 | project = None
69 |
70 | for branch in all_branches:
71 | if project != branch.project:
72 | project = branch.project
73 | out.nl()
74 | out.project(
75 | "project %s/"
76 | % project.RelPath(local=opt.this_manifest_only)
77 | )
78 | out.nl()
79 |
80 | print(
81 | "%s %-33s "
82 | % (
83 | branch.name == project.CurrentBranch and "*" or " ",
84 | branch.name,
85 | ),
86 | end="",
87 | )
88 |
89 | if not branch.base_exists:
90 | print(f"(ignoring: tracking branch is gone: {branch.base})")
91 | else:
92 | commits = branch.commits
93 | date = branch.date
94 | print(
95 | "(%2d commit%s, %s)"
96 | % (len(commits), len(commits) != 1 and "s" or " ", date)
97 | )
98 |
--------------------------------------------------------------------------------
/man/repo.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "November 2025" "repo" "Repo Manual"
3 | .SH NAME
4 | repo \- repository management tool built on top of git
5 | .SH SYNOPSIS
6 | .B repo
7 | [\fI\,-p|--paginate|--no-pager\/\fR] \fI\,COMMAND \/\fR[\fI\,ARGS\/\fR]
8 | .SH OPTIONS
9 | .TP
10 | \fB\-h\fR, \fB\-\-help\fR
11 | show this help message and exit
12 | .TP
13 | \fB\-\-help\-all\fR
14 | show this help message with all subcommands and exit
15 | .TP
16 | \fB\-p\fR, \fB\-\-paginate\fR
17 | display command output in the pager
18 | .TP
19 | \fB\-\-no\-pager\fR
20 | disable the pager
21 | .TP
22 | \fB\-\-color\fR=\fI\,COLOR\/\fR
23 | control color usage: auto, always, never
24 | .TP
25 | \fB\-\-trace\fR
26 | trace git command execution (REPO_TRACE=1)
27 | .TP
28 | \fB\-\-trace\-to\-stderr\fR
29 | trace outputs go to stderr in addition to
30 | \&.repo/TRACE_FILE
31 | .TP
32 | \fB\-\-trace\-python\fR
33 | trace python command execution
34 | .TP
35 | \fB\-\-time\fR
36 | time repo command execution
37 | .TP
38 | \fB\-\-version\fR
39 | display this version of repo
40 | .TP
41 | \fB\-\-show\-toplevel\fR
42 | display the path of the top\-level directory of the
43 | repo client checkout
44 | .TP
45 | \fB\-\-event\-log\fR=\fI\,EVENT_LOG\/\fR
46 | filename of event log to append timeline to
47 | .TP
48 | \fB\-\-git\-trace2\-event\-log\fR=\fI\,GIT_TRACE2_EVENT_LOG\/\fR
49 | directory to write git trace2 event log to
50 | .TP
51 | \fB\-\-submanifest\-path\fR=\fI\,REL_PATH\/\fR
52 | submanifest path
53 | .SS "The complete list of recognized repo commands is:"
54 | .TP
55 | abandon
56 | Permanently abandon a development branch
57 | .TP
58 | branch
59 | View current topic branches
60 | .TP
61 | branches
62 | View current topic branches
63 | .TP
64 | checkout
65 | Checkout a branch for development
66 | .TP
67 | cherry\-pick
68 | Cherry\-pick a change.
69 | .TP
70 | diff
71 | Show changes between commit and working tree
72 | .TP
73 | diffmanifests
74 | Manifest diff utility
75 | .TP
76 | download
77 | Download and checkout a change
78 | .TP
79 | forall
80 | Run a shell command in each project
81 | .TP
82 | gc
83 | Cleaning up internal repo and Git state.
84 | .TP
85 | grep
86 | Print lines matching a pattern
87 | .TP
88 | help
89 | Display detailed help on a command
90 | .TP
91 | info
92 | Get info on the manifest branch, current branch or unmerged branches
93 | .TP
94 | init
95 | Initialize a repo client checkout in the current directory
96 | .TP
97 | list
98 | List projects and their associated directories
99 | .TP
100 | manifest
101 | Manifest inspection utility
102 | .TP
103 | overview
104 | Display overview of unmerged project branches
105 | .TP
106 | prune
107 | Prune (delete) already merged topics
108 | .TP
109 | rebase
110 | Rebase local branches on upstream branch
111 | .TP
112 | selfupdate
113 | Update repo to the latest version
114 | .TP
115 | smartsync
116 | Update working tree to the latest known good revision
117 | .TP
118 | stage
119 | Stage file(s) for commit
120 | .TP
121 | start
122 | Start a new branch for development
123 | .TP
124 | status
125 | Show the working tree status
126 | .TP
127 | sync
128 | Update working tree to the latest revision
129 | .TP
130 | upload
131 | Upload changes for code review
132 | .TP
133 | version
134 | Display the version of repo
135 | .TP
136 | wipe
137 | Wipe projects from the worktree
138 | .PP
139 | See 'repo help ' for more information on a specific command.
140 | Bug reports: https://issues.gerritcodereview.com/issues/new?component=1370071
141 |
--------------------------------------------------------------------------------
/man/repo-status.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "July 2022" "repo status" "Repo Manual"
3 | .SH NAME
4 | repo \- repo status - manual page for repo status
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,status \/\fR[\fI\,\/\fR...]
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | Show the working tree status
12 | .SH OPTIONS
13 | .TP
14 | \fB\-h\fR, \fB\-\-help\fR
15 | show this help message and exit
16 | .TP
17 | \fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
18 | number of jobs to run in parallel (default: based on
19 | number of CPU cores)
20 | .TP
21 | \fB\-o\fR, \fB\-\-orphans\fR
22 | include objects in working directory outside of repo
23 | projects
24 | .SS Logging options:
25 | .TP
26 | \fB\-v\fR, \fB\-\-verbose\fR
27 | show all output
28 | .TP
29 | \fB\-q\fR, \fB\-\-quiet\fR
30 | only show errors
31 | .SS Multi\-manifest options:
32 | .TP
33 | \fB\-\-outer\-manifest\fR
34 | operate starting at the outermost manifest
35 | .TP
36 | \fB\-\-no\-outer\-manifest\fR
37 | do not operate on outer manifests
38 | .TP
39 | \fB\-\-this\-manifest\-only\fR
40 | only operate on this (sub)manifest
41 | .TP
42 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
43 | operate on this manifest and its submanifests
44 | .PP
45 | Run `repo help status` to view the detailed manual.
46 | .SH DETAILS
47 | .PP
48 | \&'repo status' compares the working tree to the staging area (aka index), and the
49 | most recent commit on this branch (HEAD), in each project specified. A summary
50 | is displayed, one line per file where there is a difference between these three
51 | states.
52 | .PP
53 | The \fB\-j\fR/\-\-jobs option can be used to run multiple status queries in parallel.
54 | .PP
55 | The \fB\-o\fR/\-\-orphans option can be used to show objects that are in the working
56 | directory, but not associated with a repo project. This includes unmanaged
57 | top\-level files and directories, but also includes deeper items. For example, if
58 | dir/subdir/proj1 and dir/subdir/proj2 are repo projects, dir/subdir/proj3 will
59 | be shown if it is not known to repo.
60 | .PP
61 | Status Display
62 | .PP
63 | The status display is organized into three columns of information, for example
64 | if the file 'subcmds/status.py' is modified in the project 'repo' on branch
65 | \&'devwork':
66 | .TP
67 | project repo/
68 | branch devwork
69 | .TP
70 | \fB\-m\fR
71 | subcmds/status.py
72 | .PP
73 | The first column explains how the staging area (index) differs from the last
74 | commit (HEAD). Its values are always displayed in upper case and have the
75 | following meanings:
76 | .TP
77 | \-:
78 | no difference
79 | .TP
80 | A:
81 | added (not in HEAD, in index )
82 | .TP
83 | M:
84 | modified ( in HEAD, in index, different content )
85 | .TP
86 | D:
87 | deleted ( in HEAD, not in index )
88 | .TP
89 | R:
90 | renamed (not in HEAD, in index, path changed )
91 | .TP
92 | C:
93 | copied (not in HEAD, in index, copied from another)
94 | .TP
95 | T:
96 | mode changed ( in HEAD, in index, same content )
97 | .TP
98 | U:
99 | unmerged; conflict resolution required
100 | .PP
101 | The second column explains how the working directory differs from the index. Its
102 | values are always displayed in lower case and have the following meanings:
103 | .TP
104 | \-:
105 | new / unknown (not in index, in work tree )
106 | .TP
107 | m:
108 | modified ( in index, in work tree, modified )
109 | .TP
110 | d:
111 | deleted ( in index, not in work tree )
112 |
--------------------------------------------------------------------------------
/hooks/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # DO NOT EDIT THIS FILE
3 | # All updates should be sent upstream: https://gerrit.googlesource.com/gerrit/
4 | # This is synced from commit: 62f5bbea67f6dafa6e22a601a0c298214c510caf
5 | # DO NOT EDIT THIS FILE
6 | #
7 | # Part of Gerrit Code Review (https://www.gerritcodereview.com/)
8 | #
9 | # Copyright (C) 2009 The Android Open Source Project
10 | #
11 | # Licensed under the Apache License, Version 2.0 (the "License");
12 | # you may not use this file except in compliance with the License.
13 | # You may obtain a copy of the License at
14 | #
15 | # http://www.apache.org/licenses/LICENSE-2.0
16 | #
17 | # Unless required by applicable law or agreed to in writing, software
18 | # distributed under the License is distributed on an "AS IS" BASIS,
19 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 | # See the License for the specific language governing permissions and
21 | # limitations under the License.
22 |
23 | set -u
24 |
25 | # avoid [[ which is not POSIX sh.
26 | if test "$#" != 1 ; then
27 | echo "$0 requires an argument."
28 | exit 1
29 | fi
30 |
31 | if test ! -f "$1" ; then
32 | echo "file does not exist: $1"
33 | exit 1
34 | fi
35 |
36 | # Do not create a change id if requested
37 | case "$(git config --get gerrit.createChangeId)" in
38 | false)
39 | exit 0
40 | ;;
41 | always)
42 | ;;
43 | *)
44 | # Do not create a change id for squash/fixup commits.
45 | if head -n1 "$1" | LC_ALL=C grep -q '^[a-z][a-z]*! '; then
46 | exit 0
47 | fi
48 | ;;
49 | esac
50 |
51 |
52 | if git rev-parse --verify HEAD >/dev/null 2>&1; then
53 | refhash="$(git rev-parse HEAD)"
54 | else
55 | refhash="$(git hash-object -t tree /dev/null)"
56 | fi
57 |
58 | random=$({ git var GIT_COMMITTER_IDENT ; echo "$refhash" ; cat "$1"; } | git hash-object --stdin)
59 | dest="$1.tmp.${random}"
60 |
61 | trap 'rm -f "$dest" "$dest-2"' EXIT
62 |
63 | if ! cat "$1" | sed -e '/>8/q' | git stripspace --strip-comments > "${dest}" ; then
64 | echo "cannot strip comments from $1"
65 | exit 1
66 | fi
67 |
68 | if test ! -s "${dest}" ; then
69 | echo "file is empty: $1"
70 | exit 1
71 | fi
72 |
73 | reviewurl="$(git config --get gerrit.reviewUrl)"
74 | if test -n "${reviewurl}" ; then
75 | token="Link"
76 | value="${reviewurl%/}/id/I$random"
77 | pattern=".*/id/I[0-9a-f]\{40\}"
78 | else
79 | token="Change-Id"
80 | value="I$random"
81 | pattern=".*"
82 | fi
83 |
84 | if git interpret-trailers --parse < "$1" | grep -q "^$token: $pattern$" ; then
85 | exit 0
86 | fi
87 |
88 | # There must be a Signed-off-by trailer for the code below to work. Insert a
89 | # sentinel at the end to make sure there is one.
90 | # Avoid the --in-place option which only appeared in Git 2.8
91 | if ! git interpret-trailers \
92 | --trailer "Signed-off-by: SENTINEL" < "$1" > "$dest-2" ; then
93 | echo "cannot insert Signed-off-by sentinel line in $1"
94 | exit 1
95 | fi
96 |
97 | # Make sure the trailer appears before any Signed-off-by trailers by inserting
98 | # it as if it was a Signed-off-by trailer and then use sed to remove the
99 | # Signed-off-by prefix and the Signed-off-by sentinel line.
100 | # Avoid the --in-place option which only appeared in Git 2.8
101 | # Avoid the --where option which only appeared in Git 2.15
102 | if ! git -c trailer.where=before interpret-trailers \
103 | --trailer "Signed-off-by: $token: $value" < "$dest-2" |
104 | sed -e "s/^Signed-off-by: \($token: \)/\1/" \
105 | -e "/^Signed-off-by: SENTINEL/d" > "$dest" ; then
106 | echo "cannot insert $token line in $1"
107 | exit 1
108 | fi
109 |
110 | if ! mv "${dest}" "$1" ; then
111 | echo "cannot mv ${dest} to $1"
112 | exit 1
113 | fi
114 |
--------------------------------------------------------------------------------
/editor.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2008 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import os
16 | import re
17 | import subprocess
18 | import sys
19 | import tempfile
20 |
21 | from error import EditorError
22 | import platform_utils
23 |
24 |
25 | class Editor:
26 | """Manages the user's preferred text editor."""
27 |
28 | _editor = None
29 | globalConfig = None
30 |
31 | @classmethod
32 | def _GetEditor(cls):
33 | if cls._editor is None:
34 | cls._editor = cls._SelectEditor()
35 | return cls._editor
36 |
37 | @classmethod
38 | def _SelectEditor(cls):
39 | e = os.getenv("GIT_EDITOR")
40 | if e:
41 | return e
42 |
43 | if cls.globalConfig:
44 | e = cls.globalConfig.GetString("core.editor")
45 | if e:
46 | return e
47 |
48 | e = os.getenv("VISUAL")
49 | if e:
50 | return e
51 |
52 | e = os.getenv("EDITOR")
53 | if e:
54 | return e
55 |
56 | if os.getenv("TERM") == "dumb":
57 | print(
58 | """No editor specified in GIT_EDITOR, core.editor, VISUAL or EDITOR.
59 | Tried to fall back to vi but terminal is dumb. Please configure at
60 | least one of these before using this command.""", # noqa: E501
61 | file=sys.stderr,
62 | )
63 | sys.exit(1)
64 |
65 | return "vi"
66 |
67 | @classmethod
68 | def EditString(cls, data):
69 | """Opens an editor to edit the given content.
70 |
71 | Args:
72 | data: The text to edit.
73 |
74 | Returns:
75 | New value of edited text.
76 |
77 | Raises:
78 | EditorError: The editor failed to run.
79 | """
80 | editor = cls._GetEditor()
81 | if editor == ":":
82 | return data
83 |
84 | fd, path = tempfile.mkstemp()
85 | try:
86 | os.write(fd, data.encode("utf-8"))
87 | os.close(fd)
88 | fd = None
89 |
90 | if platform_utils.isWindows():
91 | # Split on spaces, respecting quoted strings
92 | import shlex
93 |
94 | args = shlex.split(editor)
95 | shell = False
96 | elif re.compile("^.*[$ \t'].*$").match(editor):
97 | args = [editor + ' "$@"', "sh"]
98 | shell = True
99 | else:
100 | args = [editor]
101 | shell = False
102 | args.append(path)
103 |
104 | try:
105 | rc = subprocess.Popen(args, shell=shell).wait()
106 | except OSError as e:
107 | raise EditorError(f"editor failed, {str(e)}: {editor} {path}")
108 | if rc != 0:
109 | raise EditorError(
110 | "editor failed with exit status %d: %s %s"
111 | % (rc, editor, path)
112 | )
113 |
114 | with open(path, mode="rb") as fd2:
115 | return fd2.read().decode("utf-8")
116 | finally:
117 | if fd:
118 | os.close(fd)
119 | platform_utils.remove(path)
120 |
--------------------------------------------------------------------------------
/man/repo-grep.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "July 2022" "repo grep" "Repo Manual"
3 | .SH NAME
4 | repo \- repo grep - manual page for repo grep
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,grep {pattern | -e pattern} \/\fR[\fI\,\/\fR...]
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | Print lines matching a pattern
12 | .SH OPTIONS
13 | .TP
14 | \fB\-h\fR, \fB\-\-help\fR
15 | show this help message and exit
16 | .TP
17 | \fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
18 | number of jobs to run in parallel (default: based on
19 | number of CPU cores)
20 | .SS Logging options:
21 | .TP
22 | \fB\-\-verbose\fR
23 | show all output
24 | .TP
25 | \fB\-q\fR, \fB\-\-quiet\fR
26 | only show errors
27 | .SS Multi\-manifest options:
28 | .TP
29 | \fB\-\-outer\-manifest\fR
30 | operate starting at the outermost manifest
31 | .TP
32 | \fB\-\-no\-outer\-manifest\fR
33 | do not operate on outer manifests
34 | .TP
35 | \fB\-\-this\-manifest\-only\fR
36 | only operate on this (sub)manifest
37 | .TP
38 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
39 | operate on this manifest and its submanifests
40 | .SS Sources:
41 | .TP
42 | \fB\-\-cached\fR
43 | Search the index, instead of the work tree
44 | .TP
45 | \fB\-r\fR TREEish, \fB\-\-revision\fR=\fI\,TREEish\/\fR
46 | Search TREEish, instead of the work tree
47 | .SS Pattern:
48 | .TP
49 | \fB\-e\fR PATTERN
50 | Pattern to search for
51 | .TP
52 | \fB\-i\fR, \fB\-\-ignore\-case\fR
53 | Ignore case differences
54 | .TP
55 | \fB\-a\fR, \fB\-\-text\fR
56 | Process binary files as if they were text
57 | .TP
58 | \fB\-I\fR
59 | Don't match the pattern in binary files
60 | .TP
61 | \fB\-w\fR, \fB\-\-word\-regexp\fR
62 | Match the pattern only at word boundaries
63 | .TP
64 | \fB\-v\fR, \fB\-\-invert\-match\fR
65 | Select non\-matching lines
66 | .TP
67 | \fB\-G\fR, \fB\-\-basic\-regexp\fR
68 | Use POSIX basic regexp for patterns (default)
69 | .TP
70 | \fB\-E\fR, \fB\-\-extended\-regexp\fR
71 | Use POSIX extended regexp for patterns
72 | .TP
73 | \fB\-F\fR, \fB\-\-fixed\-strings\fR
74 | Use fixed strings (not regexp) for pattern
75 | .SS Pattern Grouping:
76 | .TP
77 | \fB\-\-all\-match\fR
78 | Limit match to lines that have all patterns
79 | .TP
80 | \fB\-\-and\fR, \fB\-\-or\fR, \fB\-\-not\fR
81 | Boolean operators to combine patterns
82 | .TP
83 | \-(, \-)
84 | Boolean operator grouping
85 | .SS Output:
86 | .TP
87 | \fB\-n\fR
88 | Prefix the line number to matching lines
89 | .TP
90 | \fB\-C\fR CONTEXT
91 | Show CONTEXT lines around match
92 | .TP
93 | \fB\-B\fR CONTEXT
94 | Show CONTEXT lines before match
95 | .TP
96 | \fB\-A\fR CONTEXT
97 | Show CONTEXT lines after match
98 | .TP
99 | \fB\-l\fR, \fB\-\-name\-only\fR, \fB\-\-files\-with\-matches\fR
100 | Show only file names containing matching lines
101 | .TP
102 | \fB\-L\fR, \fB\-\-files\-without\-match\fR
103 | Show only file names not containing matching lines
104 | .PP
105 | Run `repo help grep` to view the detailed manual.
106 | .SH DETAILS
107 | .PP
108 | Search for the specified patterns in all project files.
109 | .PP
110 | Boolean Options
111 | .PP
112 | The following options can appear as often as necessary to express the pattern to
113 | locate:
114 | .HP
115 | \fB\-e\fR PATTERN
116 | .HP
117 | \fB\-\-and\fR, \fB\-\-or\fR, \fB\-\-not\fR, \-(, \-)
118 | .PP
119 | Further, the \fB\-r\fR/\-\-revision option may be specified multiple times in order to
120 | scan multiple trees. If the same file matches in more than one tree, only the
121 | first result is reported, prefixed by the revision name it was found under.
122 | .PP
123 | Examples
124 | .PP
125 | Look for a line that has '#define' and either 'MAX_PATH or 'PATH_MAX':
126 | .IP
127 | repo grep \fB\-e\fR '#define' \fB\-\-and\fR \-\e( \fB\-e\fR MAX_PATH \fB\-e\fR PATH_MAX \e)
128 | .PP
129 | Look for a line that has 'NODE' or 'Unexpected' in files that contain a line
130 | that matches both expressions:
131 | .IP
132 | repo grep \fB\-\-all\-match\fR \fB\-e\fR NODE \fB\-e\fR Unexpected
133 |
--------------------------------------------------------------------------------
/subcmds/overview.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2012 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import optparse
16 |
17 | from color import Coloring
18 | from command import PagedCommand
19 |
20 |
21 | class Overview(PagedCommand):
22 | COMMON = True
23 | helpSummary = "Display overview of unmerged project branches"
24 | helpUsage = """
25 | %prog [--current-branch] [...]
26 | """
27 | helpDescription = """
28 | The '%prog' command is used to display an overview of the projects branches,
29 | and list any local commits that have not yet been merged into the project.
30 |
31 | The -c/--current-branch option can be used to restrict the output to only
32 | branches currently checked out in each project. By default, all branches
33 | are displayed.
34 | """
35 |
36 | def _Options(self, p):
37 | p.add_option(
38 | "-c",
39 | "--current-branch",
40 | action="store_true",
41 | help="consider only checked out branches",
42 | )
43 | p.add_option(
44 | "--no-current-branch",
45 | dest="current_branch",
46 | action="store_false",
47 | help="consider all local branches",
48 | )
49 | # Turn this into a warning & remove this someday.
50 | p.add_option(
51 | "-b",
52 | dest="current_branch",
53 | action="store_true",
54 | help=optparse.SUPPRESS_HELP,
55 | )
56 |
57 | def Execute(self, opt, args):
58 | all_branches = []
59 | for project in self.GetProjects(
60 | args, all_manifests=not opt.this_manifest_only
61 | ):
62 | br = [project.GetUploadableBranch(x) for x in project.GetBranches()]
63 | br = [x for x in br if x]
64 | if opt.current_branch:
65 | br = [x for x in br if x.name == project.CurrentBranch]
66 | all_branches.extend(br)
67 |
68 | if not all_branches:
69 | return
70 |
71 | class Report(Coloring):
72 | def __init__(self, config):
73 | Coloring.__init__(self, config, "status")
74 | self.project = self.printer("header", attr="bold")
75 | self.text = self.printer("text")
76 |
77 | out = Report(all_branches[0].project.config)
78 | out.text("Deprecated. See repo info -o.")
79 | out.nl()
80 | out.project("Projects Overview")
81 | out.nl()
82 |
83 | project = None
84 |
85 | for branch in all_branches:
86 | if project != branch.project:
87 | project = branch.project
88 | out.nl()
89 | out.project(
90 | "project %s/"
91 | % project.RelPath(local=opt.this_manifest_only)
92 | )
93 | out.nl()
94 |
95 | commits = branch.commits
96 | date = branch.date
97 | print(
98 | "%s %-33s (%2d commit%s, %s)"
99 | % (
100 | branch.name == project.CurrentBranch and "*" or " ",
101 | branch.name,
102 | len(commits),
103 | len(commits) != 1 and "s" or " ",
104 | date,
105 | )
106 | )
107 | for commit in commits:
108 | print("%-35s - %s" % ("", commit))
109 |
--------------------------------------------------------------------------------
/tests/test_repo_logging.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2023 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Unit test for repo_logging module."""
16 |
17 | import contextlib
18 | import io
19 | import logging
20 | import unittest
21 | from unittest import mock
22 |
23 | from color import SetDefaultColoring
24 | from error import RepoExitError
25 | from repo_logging import RepoLogger
26 |
27 |
28 | class TestRepoLogger(unittest.TestCase):
29 | @mock.patch.object(RepoLogger, "error")
30 | def test_log_aggregated_errors_logs_aggregated_errors(self, mock_error):
31 | """Test if log_aggregated_errors logs a list of aggregated errors."""
32 | logger = RepoLogger(__name__)
33 | logger.log_aggregated_errors(
34 | RepoExitError(
35 | aggregate_errors=[
36 | Exception("foo"),
37 | Exception("bar"),
38 | Exception("baz"),
39 | Exception("hello"),
40 | Exception("world"),
41 | Exception("test"),
42 | ]
43 | )
44 | )
45 |
46 | mock_error.assert_has_calls(
47 | [
48 | mock.call("=" * 80),
49 | mock.call(
50 | "Repo command failed due to the following `%s` errors:",
51 | "RepoExitError",
52 | ),
53 | mock.call("foo\nbar\nbaz\nhello\nworld"),
54 | mock.call("+%d additional errors...", 1),
55 | ]
56 | )
57 |
58 | @mock.patch.object(RepoLogger, "error")
59 | def test_log_aggregated_errors_logs_single_error(self, mock_error):
60 | """Test if log_aggregated_errors logs empty aggregated_errors."""
61 | logger = RepoLogger(__name__)
62 | logger.log_aggregated_errors(RepoExitError())
63 |
64 | mock_error.assert_has_calls(
65 | [
66 | mock.call("=" * 80),
67 | mock.call("Repo command failed: %s", "RepoExitError"),
68 | ]
69 | )
70 |
71 | def test_log_with_format_string(self):
72 | """Test different log levels with format strings."""
73 |
74 | # Set color output to "always" for consistent test results.
75 | # This ensures the logger's behavior is uniform across different
76 | # environments and git configurations.
77 | SetDefaultColoring("always")
78 |
79 | # Regex pattern to match optional ANSI color codes.
80 | # \033 - Escape character
81 | # \[ - Opening square bracket
82 | # [0-9;]* - Zero or more digits or semicolons
83 | # m - Ending 'm' character
84 | # ? - Makes the entire group optional
85 | opt_color = r"(\033\[[0-9;]*m)?"
86 |
87 | for level in (logging.INFO, logging.WARN, logging.ERROR):
88 | name = logging.getLevelName(level)
89 |
90 | with self.subTest(level=level, name=name):
91 | output = io.StringIO()
92 |
93 | with contextlib.redirect_stderr(output):
94 | logger = RepoLogger(__name__)
95 | logger.log(level, "%s", "100% pass")
96 |
97 | self.assertRegex(
98 | output.getvalue().strip(),
99 | f"^{opt_color}100% pass{opt_color}$",
100 | f"failed for level {name}",
101 | )
102 |
--------------------------------------------------------------------------------
/subcmds/checkout.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2009 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import functools
16 | from typing import NamedTuple
17 |
18 | from command import Command
19 | from command import DEFAULT_LOCAL_JOBS
20 | from error import GitError
21 | from error import RepoExitError
22 | from progress import Progress
23 | from repo_logging import RepoLogger
24 |
25 |
26 | logger = RepoLogger(__file__)
27 |
28 |
29 | class CheckoutBranchResult(NamedTuple):
30 | # Whether the Project is on the branch (i.e. branch exists and no errors)
31 | result: bool
32 | project_idx: int
33 | error: Exception
34 |
35 |
36 | class CheckoutCommandError(RepoExitError):
37 | """Exception thrown when checkout command fails."""
38 |
39 |
40 | class MissingBranchError(RepoExitError):
41 | """Exception thrown when no project has specified branch."""
42 |
43 |
44 | class Checkout(Command):
45 | COMMON = True
46 | helpSummary = "Checkout a branch for development"
47 | helpUsage = """
48 | %prog [...]
49 | """
50 | helpDescription = """
51 | The '%prog' command checks out an existing branch that was previously
52 | created by 'repo start'.
53 |
54 | The command is equivalent to:
55 |
56 | repo forall [...] -c git checkout
57 | """
58 | PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
59 |
60 | def ValidateOptions(self, opt, args):
61 | if not args:
62 | self.Usage()
63 |
64 | @classmethod
65 | def _ExecuteOne(cls, nb, project_idx):
66 | """Checkout one project."""
67 | error = None
68 | result = None
69 | project = cls.get_parallel_context()["projects"][project_idx]
70 | try:
71 | result = project.CheckoutBranch(nb)
72 | except GitError as e:
73 | error = e
74 | return CheckoutBranchResult(result, project_idx, error)
75 |
76 | def Execute(self, opt, args):
77 | nb = args[0]
78 | err = []
79 | err_projects = []
80 | success = []
81 | all_projects = self.GetProjects(
82 | args[1:], all_manifests=not opt.this_manifest_only
83 | )
84 |
85 | def _ProcessResults(_pool, pm, results):
86 | for result in results:
87 | project = all_projects[result.project_idx]
88 | if result.error is not None:
89 | err.append(result.error)
90 | err_projects.append(project)
91 | elif result.result:
92 | success.append(project)
93 | pm.update(msg="")
94 |
95 | with self.ParallelContext():
96 | self.get_parallel_context()["projects"] = all_projects
97 | self.ExecuteInParallel(
98 | opt.jobs,
99 | functools.partial(self._ExecuteOne, nb),
100 | range(len(all_projects)),
101 | callback=_ProcessResults,
102 | output=Progress(
103 | f"Checkout {nb}", len(all_projects), quiet=opt.quiet
104 | ),
105 | )
106 |
107 | if err_projects:
108 | for p in err_projects:
109 | logger.error("error: %s/: cannot checkout %s", p.relpath, nb)
110 | raise CheckoutCommandError(aggregate_errors=err)
111 | elif not success:
112 | msg = f"error: no project has branch {nb}"
113 | logger.error(msg)
114 | raise MissingBranchError(msg)
115 |
--------------------------------------------------------------------------------
/pager.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2008 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import os
16 | import select
17 | import subprocess
18 | import sys
19 |
20 | import platform_utils
21 |
22 |
23 | active = False
24 | pager_process = None
25 | old_stdout = None
26 | old_stderr = None
27 |
28 |
29 | def RunPager(globalConfig):
30 | if not os.isatty(0) or not os.isatty(1):
31 | return
32 | pager = _SelectPager(globalConfig)
33 | if pager == "" or pager == "cat":
34 | return
35 |
36 | if platform_utils.isWindows():
37 | _PipePager(pager)
38 | else:
39 | _ForkPager(pager)
40 |
41 |
42 | def TerminatePager():
43 | global pager_process
44 | if pager_process:
45 | sys.stdout.flush()
46 | sys.stderr.flush()
47 | pager_process.stdin.close()
48 | pager_process.wait()
49 | pager_process = None
50 | # Restore initial stdout/err in case there is more output in this
51 | # process after shutting down the pager process.
52 | sys.stdout = old_stdout
53 | sys.stderr = old_stderr
54 |
55 |
56 | def _PipePager(pager):
57 | global pager_process, old_stdout, old_stderr
58 | assert pager_process is None, "Only one active pager process at a time"
59 | # Create pager process, piping stdout/err into its stdin.
60 | try:
61 | pager_process = subprocess.Popen(
62 | [pager], stdin=subprocess.PIPE, stdout=sys.stdout, stderr=sys.stderr
63 | )
64 | except FileNotFoundError:
65 | sys.exit(f'fatal: cannot start pager "{pager}"')
66 | old_stdout = sys.stdout
67 | old_stderr = sys.stderr
68 | sys.stdout = pager_process.stdin
69 | sys.stderr = pager_process.stdin
70 |
71 |
72 | def _ForkPager(pager):
73 | global active
74 | # This process turns into the pager; a child it forks will
75 | # do the real processing and output back to the pager. This
76 | # is necessary to keep the pager in control of the tty.
77 | try:
78 | r, w = os.pipe()
79 | pid = os.fork()
80 | if not pid:
81 | os.dup2(w, 1)
82 | os.dup2(w, 2)
83 | os.close(r)
84 | os.close(w)
85 | active = True
86 | return
87 |
88 | os.dup2(r, 0)
89 | os.close(r)
90 | os.close(w)
91 |
92 | _BecomePager(pager)
93 | except Exception:
94 | print("fatal: cannot start pager '%s'" % pager, file=sys.stderr)
95 | sys.exit(255)
96 |
97 |
98 | def _SelectPager(globalConfig):
99 | try:
100 | return os.environ["GIT_PAGER"]
101 | except KeyError:
102 | pass
103 |
104 | pager = globalConfig.GetString("core.pager")
105 | if pager:
106 | return pager
107 |
108 | try:
109 | return os.environ["PAGER"]
110 | except KeyError:
111 | pass
112 |
113 | return "less"
114 |
115 |
116 | def _BecomePager(pager):
117 | # Delaying execution of the pager until we have output
118 | # ready works around a long-standing bug in popularly
119 | # available versions of 'less', a better 'more'.
120 | _a, _b, _c = select.select([0], [], [0])
121 |
122 | # This matches the behavior of git, which sets $LESS to `FRX` if it is not
123 | # set. See:
124 | # https://git-scm.com/docs/git-config#Documentation/git-config.txt-corepager
125 | os.environ.setdefault("LESS", "FRX")
126 |
127 | try:
128 | os.execvp(pager, [pager])
129 | except OSError:
130 | os.execv("/bin/sh", ["sh", "-c", pager])
131 |
--------------------------------------------------------------------------------
/docs/python-support.md:
--------------------------------------------------------------------------------
1 | # Supported Python Versions
2 |
3 | This documents the current supported Python versions, and tries to provide
4 | guidance for when we decide to drop support for older versions.
5 |
6 | ## Summary
7 |
8 | * Python 3.6 (released Dec 2016) is required starting with repo-2.0.
9 | * Older versions of Python (e.g. v2.7) may use old releases via the repo-1.x
10 | branch, but no support is provided.
11 |
12 | ## repo hooks
13 |
14 | Projects that use [repo hooks] run on independent schedules.
15 | Since it's not possible to detect what version of Python the hooks were written
16 | or tested against, we always import & exec them with the active Python version.
17 |
18 | If the user's Python is too new for the [repo hooks], then it is up to the hooks
19 | maintainer to update.
20 |
21 | ## Repo launcher
22 |
23 | The [repo launcher] is an independent script that can support older versions of
24 | Python without holding back the rest of the codebase.
25 | If it detects the current version of Python is too old, it will try to reexec
26 | via a newer version of Python via standard `pythonX.Y` interpreter names.
27 |
28 | However, this is provided as a nicety when it is not onerous, and there is no
29 | official support for older versions of Python than the rest of the codebase.
30 |
31 | If your default python interpreters are too old to run the launcher even though
32 | you have newer versions installed, your choices are:
33 |
34 | * Modify the [repo launcher]'s shebang to suite your environment.
35 | * Download an older version of the [repo launcher] and don't upgrade it.
36 | Be aware that we do not guarantee old repo launchers will work with current
37 | versions of repo. Bug reports using old launchers will not be accepted.
38 |
39 | ## When to drop support
40 |
41 | So far, Python 3.6 has provided most of the interesting features that we want
42 | (e.g. typing & f-strings), and there haven't been features in newer versions
43 | that are critical to us.
44 |
45 | That said, let's assume we need functionality that only exists in Python 3.7.
46 | How do we decide when it's acceptable to drop Python 3.6?
47 |
48 | 1. Review the [Project References](./release-process.md#project-references) to
49 | see what major distros are using the previous version of Python, and when
50 | they go EOL. Generally we care about Ubuntu LTS & current/previous Debian
51 | stable versions.
52 | * If they're all EOL already, then go for it, drop support.
53 | * If they aren't EOL, start a thread on [repo-discuss] to see how the user
54 | base feels about the proposal.
55 | 1. Update the "soft" versions in the codebase. This will start warning users
56 | that the older version is deprecated.
57 | * Update [repo](/repo) if the launcher needs updating.
58 | This only helps with people who download newer launchers.
59 | * Update [main.py](/main.py) for the main codebase.
60 | This warns for everyone regardless of [repo launcher] version.
61 | * Update [requirements.json](/requirements.json).
62 | This allows [repo launcher] to display warnings/errors without having
63 | to execute the new codebase. This helps in case of syntax or module
64 | changes where older versions won't even be able to import the new code.
65 | 1. After some grace period (ideally at least 2 quarters after the first release
66 | with the updated soft requirements), update the "hard" versions, and then
67 | start using the new functionality.
68 |
69 | ## Python 2.7 & 3.0-3.5
70 |
71 | > **There is no support for these versions.**
72 | > **Do not file bugs if you are using old Python versions.**
73 | > **Any such reports will be marked invalid and ignored.**
74 | > **Upgrade your distro and/or runtime instead.**
75 |
76 | Fetch an old version of the [repo launcher]:
77 |
78 | ```sh
79 | $ curl https://storage.googleapis.com/git-repo-downloads/repo-2.32 > ~/.bin/repo-2.32
80 | $ chmod a+rx ~/.bin/repo-2.32
81 | ```
82 |
83 | Then initialize an old version of repo:
84 |
85 | ```sh
86 | $ repo-2.32 init --repo-rev=repo-1 ...
87 | ```
88 |
89 |
90 | [repo-discuss]: https://groups.google.com/forum/#!forum/repo-discuss
91 | [repo hooks]: ./repo-hooks.md
92 | [repo launcher]: ../repo
93 |
--------------------------------------------------------------------------------
/subcmds/stage.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2008 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import sys
16 |
17 | from color import Coloring
18 | from command import InteractiveCommand
19 | from git_command import GitCommand
20 | from repo_logging import RepoLogger
21 |
22 |
23 | logger = RepoLogger(__file__)
24 |
25 |
26 | class _ProjectList(Coloring):
27 | def __init__(self, gc):
28 | Coloring.__init__(self, gc, "interactive")
29 | self.prompt = self.printer("prompt", fg="blue", attr="bold")
30 | self.header = self.printer("header", attr="bold")
31 | self.help = self.printer("help", fg="red", attr="bold")
32 |
33 |
34 | class Stage(InteractiveCommand):
35 | COMMON = True
36 | helpSummary = "Stage file(s) for commit"
37 | helpUsage = """
38 | %prog -i [...]
39 | """
40 | helpDescription = """
41 | The '%prog' command stages files to prepare the next commit.
42 | """
43 |
44 | def _Options(self, p):
45 | g = p.get_option_group("--quiet")
46 | g.add_option(
47 | "-i",
48 | "--interactive",
49 | action="store_true",
50 | help="use interactive staging",
51 | )
52 |
53 | def Execute(self, opt, args):
54 | if opt.interactive:
55 | self._Interactive(opt, args)
56 | else:
57 | self.Usage()
58 |
59 | def _Interactive(self, opt, args):
60 | all_projects = [
61 | p
62 | for p in self.GetProjects(
63 | args, all_manifests=not opt.this_manifest_only
64 | )
65 | if p.IsDirty()
66 | ]
67 | if not all_projects:
68 | logger.error("no projects have uncommitted modifications")
69 | return
70 |
71 | out = _ProjectList(self.manifest.manifestProject.config)
72 | while True:
73 | out.header(" %s", "project")
74 | out.nl()
75 |
76 | for i in range(len(all_projects)):
77 | project = all_projects[i]
78 | out.write(
79 | "%3d: %s",
80 | i + 1,
81 | project.RelPath(local=opt.this_manifest_only) + "/",
82 | )
83 | out.nl()
84 | out.nl()
85 |
86 | out.write("%3d: (", 0)
87 | out.prompt("q")
88 | out.write("uit)")
89 | out.nl()
90 |
91 | out.prompt("project> ")
92 | out.flush()
93 | try:
94 | a = sys.stdin.readline()
95 | except KeyboardInterrupt:
96 | out.nl()
97 | break
98 | if a == "":
99 | out.nl()
100 | break
101 |
102 | a = a.strip()
103 | if a.lower() in ("q", "quit", "exit"):
104 | break
105 | if not a:
106 | continue
107 |
108 | try:
109 | a_index = int(a)
110 | except ValueError:
111 | a_index = None
112 |
113 | if a_index is not None:
114 | if a_index == 0:
115 | break
116 | if 0 < a_index and a_index <= len(all_projects):
117 | _AddI(all_projects[a_index - 1])
118 | continue
119 |
120 | projects = [
121 | p
122 | for p in all_projects
123 | if a in [p.name, p.RelPath(local=opt.this_manifest_only)]
124 | ]
125 | if len(projects) == 1:
126 | _AddI(projects[0])
127 | continue
128 | print("Bye.")
129 |
130 |
131 | def _AddI(project):
132 | p = GitCommand(project, ["add", "--interactive"], bare=False)
133 | p.Wait()
134 |
--------------------------------------------------------------------------------
/subcmds/cherry_pick.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2010 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import re
16 | import sys
17 |
18 | from command import Command
19 | from error import GitError
20 | from git_command import GitCommand
21 | from repo_logging import RepoLogger
22 |
23 |
24 | CHANGE_ID_RE = re.compile(r"^\s*Change-Id: I([0-9a-f]{40})\s*$")
25 | logger = RepoLogger(__file__)
26 |
27 |
28 | class CherryPick(Command):
29 | COMMON = True
30 | helpSummary = "Cherry-pick a change."
31 | helpUsage = """
32 | %prog
33 | """
34 | helpDescription = """
35 | '%prog' cherry-picks a change from one branch to another.
36 | The change id will be updated, and a reference to the old
37 | change id will be added.
38 | """
39 |
40 | def ValidateOptions(self, opt, args):
41 | if len(args) != 1:
42 | self.Usage()
43 |
44 | def Execute(self, opt, args):
45 | reference = args[0]
46 |
47 | p = GitCommand(
48 | None,
49 | ["rev-parse", "--verify", reference],
50 | capture_stdout=True,
51 | capture_stderr=True,
52 | verify_command=True,
53 | )
54 | try:
55 | p.Wait()
56 | except GitError:
57 | logger.error(p.stderr)
58 | raise
59 |
60 | sha1 = p.stdout.strip()
61 |
62 | p = GitCommand(
63 | None,
64 | ["cat-file", "commit", sha1],
65 | capture_stdout=True,
66 | verify_command=True,
67 | )
68 |
69 | try:
70 | p.Wait()
71 | except GitError:
72 | logger.error("error: Failed to retrieve old commit message")
73 | raise
74 |
75 | old_msg = self._StripHeader(p.stdout)
76 |
77 | p = GitCommand(
78 | None,
79 | ["cherry-pick", sha1],
80 | capture_stdout=True,
81 | capture_stderr=True,
82 | verify_command=True,
83 | )
84 |
85 | try:
86 | p.Wait()
87 | except GitError as e:
88 | logger.error(e)
89 | logger.warning(
90 | "NOTE: When committing (please see above) and editing the "
91 | "commit message, please remove the old Change-Id-line and "
92 | "add:\n%s",
93 | self._GetReference(sha1),
94 | )
95 | raise
96 |
97 | if p.stdout:
98 | print(p.stdout.strip(), file=sys.stdout)
99 | if p.stderr:
100 | print(p.stderr.strip(), file=sys.stderr)
101 |
102 | # The cherry-pick was applied correctly. We just need to edit
103 | # the commit message.
104 | new_msg = self._Reformat(old_msg, sha1)
105 |
106 | p = GitCommand(
107 | None,
108 | ["commit", "--amend", "-F", "-"],
109 | input=new_msg,
110 | capture_stdout=True,
111 | capture_stderr=True,
112 | verify_command=True,
113 | )
114 | try:
115 | p.Wait()
116 | except GitError:
117 | logger.error("error: Failed to update commit message")
118 | raise
119 |
120 | def _IsChangeId(self, line):
121 | return CHANGE_ID_RE.match(line)
122 |
123 | def _GetReference(self, sha1):
124 | return "(cherry picked from commit %s)" % sha1
125 |
126 | def _StripHeader(self, commit_msg):
127 | lines = commit_msg.splitlines()
128 | return "\n".join(lines[lines.index("") + 1 :])
129 |
130 | def _Reformat(self, old_msg, sha1):
131 | new_msg = []
132 |
133 | for line in old_msg.splitlines():
134 | if not self._IsChangeId(line):
135 | new_msg.append(line)
136 |
137 | # Add a blank line between the message and the change id/reference.
138 | try:
139 | if new_msg[-1].strip() != "":
140 | new_msg.append("")
141 | except IndexError:
142 | pass
143 |
144 | new_msg.append(self._GetReference(sha1))
145 | return "\n".join(new_msg)
146 |
--------------------------------------------------------------------------------
/release/update-hooks:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # Copyright (C) 2024 The Android Open Source Project
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | """Helper tool for updating hooks from their various upstreams."""
17 |
18 | import argparse
19 | import base64
20 | import json
21 | from pathlib import Path
22 | import sys
23 | from typing import List, Optional
24 | import urllib.request
25 |
26 |
27 | assert sys.version_info >= (3, 8), "Python 3.8+ required"
28 |
29 |
30 | TOPDIR = Path(__file__).resolve().parent.parent
31 | HOOKS_DIR = TOPDIR / "hooks"
32 |
33 |
34 | def update_hook_commit_msg() -> None:
35 | """Update commit-msg hook from Gerrit."""
36 | hook = HOOKS_DIR / "commit-msg"
37 | print(
38 | f"{hook.name}: Updating from https://gerrit.googlesource.com/gerrit/"
39 | "+/HEAD/resources/com/google/gerrit/server/tools/root/hooks/commit-msg"
40 | )
41 |
42 | # Get the current commit.
43 | url = "https://gerrit.googlesource.com/gerrit/+/HEAD?format=JSON"
44 | with urllib.request.urlopen(url) as fp:
45 | data = fp.read()
46 | # Discard the xss protection.
47 | data = data.split(b"\n", 1)[1]
48 | data = json.loads(data)
49 | commit = data["commit"]
50 |
51 | # Fetch the data for that commit.
52 | url = (
53 | f"https://gerrit.googlesource.com/gerrit/+/{commit}/"
54 | "resources/com/google/gerrit/server/tools/root/hooks/commit-msg"
55 | )
56 | with urllib.request.urlopen(f"{url}?format=TEXT") as fp:
57 | data = fp.read()
58 |
59 | # gitiles base64 encodes text data.
60 | data = base64.b64decode(data)
61 |
62 | # Inject header into the hook.
63 | lines = data.split(b"\n")
64 | lines = (
65 | lines[:1]
66 | + [
67 | b"# DO NOT EDIT THIS FILE",
68 | (
69 | b"# All updates should be sent upstream: "
70 | b"https://gerrit.googlesource.com/gerrit/"
71 | ),
72 | f"# This is synced from commit: {commit}".encode("utf-8"),
73 | b"# DO NOT EDIT THIS FILE",
74 | ]
75 | + lines[1:]
76 | )
77 | data = b"\n".join(lines)
78 |
79 | # Update the hook.
80 | hook.write_bytes(data)
81 | hook.chmod(0o755)
82 |
83 |
84 | def update_hook_pre_auto_gc() -> None:
85 | """Update pre-auto-gc hook from git."""
86 | hook = HOOKS_DIR / "pre-auto-gc"
87 | print(
88 | f"{hook.name}: Updating from https://github.com/git/git/"
89 | "HEAD/contrib/hooks/pre-auto-gc-battery"
90 | )
91 |
92 | # Get the current commit.
93 | headers = {
94 | "Accept": "application/vnd.github+json",
95 | "X-GitHub-Api-Version": "2022-11-28",
96 | }
97 | url = "https://api.github.com/repos/git/git/git/refs/heads/master"
98 | req = urllib.request.Request(url, headers=headers)
99 | with urllib.request.urlopen(req) as fp:
100 | data = fp.read()
101 | data = json.loads(data)
102 |
103 | # Fetch the data for that commit.
104 | commit = data["object"]["sha"]
105 | url = (
106 | f"https://raw.githubusercontent.com/git/git/{commit}/"
107 | "contrib/hooks/pre-auto-gc-battery"
108 | )
109 | with urllib.request.urlopen(url) as fp:
110 | data = fp.read()
111 |
112 | # Inject header into the hook.
113 | lines = data.split(b"\n")
114 | lines = (
115 | lines[:1]
116 | + [
117 | b"# DO NOT EDIT THIS FILE",
118 | (
119 | b"# All updates should be sent upstream: "
120 | b"https://github.com/git/git/"
121 | ),
122 | f"# This is synced from commit: {commit}".encode("utf-8"),
123 | b"# DO NOT EDIT THIS FILE",
124 | ]
125 | + lines[1:]
126 | )
127 | data = b"\n".join(lines)
128 |
129 | # Update the hook.
130 | hook.write_bytes(data)
131 | hook.chmod(0o755)
132 |
133 |
134 | def main(argv: Optional[List[str]] = None) -> Optional[int]:
135 | parser = argparse.ArgumentParser(description=__doc__)
136 | parser.parse_args(argv)
137 |
138 | update_hook_commit_msg()
139 | update_hook_pre_auto_gc()
140 |
141 |
142 | if __name__ == "__main__":
143 | sys.exit(main(sys.argv[1:]))
144 |
--------------------------------------------------------------------------------
/subcmds/list.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2011 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import os
16 |
17 | from command import Command
18 | from command import MirrorSafeCommand
19 |
20 |
21 | class List(Command, MirrorSafeCommand):
22 | COMMON = True
23 | helpSummary = "List projects and their associated directories"
24 | helpUsage = """
25 | %prog [-f] [...]
26 | %prog [-f] -r str1 [str2]...
27 | """
28 | helpDescription = """
29 | List all projects; pass '.' to list the project for the cwd.
30 |
31 | By default, only projects that currently exist in the checkout are shown. If
32 | you want to list all projects (using the specified filter settings), use the
33 | --all option. If you want to show all projects regardless of the manifest
34 | groups, then also pass --groups all.
35 |
36 | This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
37 | """
38 |
39 | def _Options(self, p):
40 | p.add_option(
41 | "-r",
42 | "--regex",
43 | action="store_true",
44 | help="filter the project list based on regex or wildcard matching "
45 | "of strings",
46 | )
47 | p.add_option(
48 | "-g",
49 | "--groups",
50 | help="filter the project list based on the groups the project is "
51 | "in",
52 | )
53 | p.add_option(
54 | "-a",
55 | "--all",
56 | action="store_true",
57 | help="show projects regardless of checkout state",
58 | )
59 | p.add_option(
60 | "-n",
61 | "--name-only",
62 | action="store_true",
63 | help="display only the name of the repository",
64 | )
65 | p.add_option(
66 | "-p",
67 | "--path-only",
68 | action="store_true",
69 | help="display only the path of the repository",
70 | )
71 | p.add_option(
72 | "-f",
73 | "--fullpath",
74 | action="store_true",
75 | help="display the full work tree path instead of the relative path",
76 | )
77 | p.add_option(
78 | "--relative-to",
79 | metavar="PATH",
80 | help="display paths relative to this one (default: top of repo "
81 | "client checkout)",
82 | )
83 |
84 | def ValidateOptions(self, opt, args):
85 | if opt.fullpath and opt.name_only:
86 | self.OptionParser.error("cannot combine -f and -n")
87 |
88 | # Resolve any symlinks so the output is stable.
89 | if opt.relative_to:
90 | opt.relative_to = os.path.realpath(opt.relative_to)
91 |
92 | def Execute(self, opt, args):
93 | """List all projects and the associated directories.
94 |
95 | This may be possible to do with 'repo forall', but repo newbies have
96 | trouble figuring that out. The idea here is that it should be more
97 | discoverable.
98 |
99 | Args:
100 | opt: The options.
101 | args: Positional args. Can be a list of projects to list, or empty.
102 | """
103 | if not opt.regex:
104 | projects = self.GetProjects(
105 | args,
106 | groups=opt.groups,
107 | missing_ok=opt.all,
108 | all_manifests=not opt.this_manifest_only,
109 | )
110 | else:
111 | projects = self.FindProjects(
112 | args, all_manifests=not opt.this_manifest_only
113 | )
114 |
115 | def _getpath(x):
116 | if opt.fullpath:
117 | return x.worktree
118 | if opt.relative_to:
119 | return os.path.relpath(x.worktree, opt.relative_to)
120 | return x.RelPath(local=opt.this_manifest_only)
121 |
122 | lines = []
123 | for project in projects:
124 | if opt.name_only and not opt.path_only:
125 | lines.append("%s" % (project.name))
126 | elif opt.path_only and not opt.name_only:
127 | lines.append("%s" % (_getpath(project)))
128 | else:
129 | lines.append(f"{_getpath(project)} : {project.name}")
130 |
131 | if lines:
132 | lines.sort()
133 | print("\n".join(lines))
134 |
--------------------------------------------------------------------------------
/completion.bash:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2021 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | # Programmable bash completion. https://github.com/scop/bash-completion
16 |
17 | # TODO: Handle interspersed options. We handle `repo h`, but not
18 | # `repo --time h`.
19 |
20 | # Complete the list of repo subcommands.
21 | __complete_repo_list_commands() {
22 | local repo=${COMP_WORDS[0]}
23 | (
24 | # Handle completions if running outside of a checkout.
25 | if ! "${repo}" help --all 2>/dev/null; then
26 | repo help 2>/dev/null
27 | fi
28 | ) | sed -n '/^ /{s/ \([^ ]\+\) .\+/\1/;p}'
29 | }
30 |
31 | # Complete list of all branches available in all projects in the repo client
32 | # checkout.
33 | __complete_repo_list_branches() {
34 | local repo=${COMP_WORDS[0]}
35 | "${repo}" branches 2>/dev/null | \
36 | sed -n '/|/{s/[ *][Pp ] *\([^ ]\+\) .*/\1/;p}'
37 | }
38 |
39 | # Complete list of all projects available in the repo client checkout.
40 | __complete_repo_list_projects() {
41 | local repo=${COMP_WORDS[0]}
42 | "${repo}" list -n 2>/dev/null
43 | "${repo}" list -p --relative-to=. 2>/dev/null
44 | }
45 |
46 | # Complete the repo argument.
47 | __complete_repo_command() {
48 | if [[ ${COMP_CWORD} -ne 1 ]]; then
49 | return 1
50 | fi
51 |
52 | local command=${COMP_WORDS[1]}
53 | COMPREPLY=($(compgen -W "$(__complete_repo_list_commands)" -- "${command}"))
54 | return 0
55 | }
56 |
57 | # Complete repo subcommands that take .
58 | __complete_repo_command_branch_projects() {
59 | local current=$1
60 | if [[ ${COMP_CWORD} -eq 2 ]]; then
61 | COMPREPLY=($(compgen -W "$(__complete_repo_list_branches)" -- "${current}"))
62 | else
63 | COMPREPLY=($(compgen -W "$(__complete_repo_list_projects)" -- "${current}"))
64 | fi
65 | }
66 |
67 | # Complete repo subcommands that take only .
68 | __complete_repo_command_projects() {
69 | local current=$1
70 | COMPREPLY=($(compgen -W "$(__complete_repo_list_projects)" -- "${current}"))
71 | }
72 |
73 | # Complete `repo help`.
74 | __complete_repo_command_help() {
75 | local current=$1
76 | # CWORD=1 is "start".
77 | # CWORD=2 is the which we complete here.
78 | if [[ ${COMP_CWORD} -eq 2 ]]; then
79 | COMPREPLY=(
80 | $(compgen -W "$(__complete_repo_list_commands)" -- "${current}")
81 | )
82 | fi
83 | }
84 |
85 | # Complete `repo forall`.
86 | __complete_repo_command_forall() {
87 | local current=$1
88 | # CWORD=1 is "forall".
89 | # CWORD=2+ are *until* we hit the -c option.
90 | local i
91 | for (( i = 0; i < COMP_CWORD; ++i )); do
92 | if [[ "${COMP_WORDS[i]}" == "-c" ]]; then
93 | return 0
94 | fi
95 | done
96 |
97 | COMPREPLY=(
98 | $(compgen -W "$(__complete_repo_list_projects)" -- "${current}")
99 | )
100 | }
101 |
102 | # Complete `repo start`.
103 | __complete_repo_command_start() {
104 | local current=$1
105 | # CWORD=1 is "start".
106 | # CWORD=2 is the which we don't complete.
107 | # CWORD=3+ are which we complete here.
108 | if [[ ${COMP_CWORD} -gt 2 ]]; then
109 | COMPREPLY=(
110 | $(compgen -W "$(__complete_repo_list_projects)" -- "${current}")
111 | )
112 | fi
113 | }
114 |
115 | # Complete the repo subcommand arguments.
116 | __complete_repo_arg() {
117 | if [[ ${COMP_CWORD} -le 1 ]]; then
118 | return 1
119 | fi
120 |
121 | local command=${COMP_WORDS[1]}
122 | local current=${COMP_WORDS[COMP_CWORD]}
123 | case ${command} in
124 | abandon|checkout)
125 | __complete_repo_command_branch_projects "${current}"
126 | return 0
127 | ;;
128 |
129 | branch|branches|diff|info|list|overview|prune|rebase|smartsync|stage|status|\
130 | sync|upload)
131 | __complete_repo_command_projects "${current}"
132 | return 0
133 | ;;
134 |
135 | help|start|forall)
136 | __complete_repo_command_${command} "${current}"
137 | return 0
138 | ;;
139 |
140 | *)
141 | return 1
142 | ;;
143 | esac
144 | }
145 |
146 | # Complete the repo arguments.
147 | __complete_repo() {
148 | COMPREPLY=()
149 | __complete_repo_command && return 0
150 | __complete_repo_arg && return 0
151 | return 0
152 | }
153 |
154 | # Fallback to the default complete methods if we aren't able to provide anything
155 | # useful. This will allow e.g. local paths to be used when it makes sense.
156 | complete -F __complete_repo -o bashdefault -o default repo
157 |
--------------------------------------------------------------------------------
/release/check-metadata.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # Copyright (C) 2025 The Android Open Source Project
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | """Helper tool to check various metadata (e.g. licensing) in source files."""
17 |
18 | import argparse
19 | from pathlib import Path
20 | import re
21 | import sys
22 |
23 | import util
24 |
25 |
26 | _FILE_HEADER_RE = re.compile(
27 | r"""# Copyright \(C\) 20[0-9]{2} The Android Open Source Project
28 | #
29 | # Licensed under the Apache License, Version 2\.0 \(the "License"\);
30 | # you may not use this file except in compliance with the License\.
31 | # You may obtain a copy of the License at
32 | #
33 | # http://www\.apache\.org/licenses/LICENSE-2\.0
34 | #
35 | # Unless required by applicable law or agreed to in writing, software
36 | # distributed under the License is distributed on an "AS IS" BASIS,
37 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.
38 | # See the License for the specific language governing permissions and
39 | # limitations under the License\.
40 | """
41 | )
42 |
43 |
44 | def check_license(path: Path, lines: list[str]) -> bool:
45 | """Check license header."""
46 | # Enforce licensing on configs & scripts.
47 | if not (
48 | path.suffix in (".bash", ".cfg", ".ini", ".py", ".toml")
49 | or lines[0] in ("#!/bin/bash", "#!/bin/sh", "#!/usr/bin/env python3")
50 | ):
51 | return True
52 |
53 | # Extract the file header.
54 | header_lines = []
55 | for line in lines:
56 | if line.startswith("#"):
57 | header_lines.append(line)
58 | else:
59 | break
60 | if not header_lines:
61 | print(
62 | f"error: {path.relative_to(util.TOPDIR)}: "
63 | "missing file header (copyright+licensing)",
64 | file=sys.stderr,
65 | )
66 | return False
67 |
68 | # Skip the shebang.
69 | if header_lines[0].startswith("#!"):
70 | header_lines.pop(0)
71 |
72 | # If this file is imported into the tree, then leave it be.
73 | if header_lines[0] == "# DO NOT EDIT THIS FILE":
74 | return True
75 |
76 | header = "".join(f"{x}\n" for x in header_lines)
77 | if not _FILE_HEADER_RE.match(header):
78 | print(
79 | f"error: {path.relative_to(util.TOPDIR)}: "
80 | "file header incorrectly formatted",
81 | file=sys.stderr,
82 | )
83 | print(
84 | "".join(f"> {x}\n" for x in header_lines), end="", file=sys.stderr
85 | )
86 | return False
87 |
88 | return True
89 |
90 |
91 | def check_path(opts: argparse.Namespace, path: Path) -> bool:
92 | """Check a single path."""
93 | data = path.read_text(encoding="utf-8")
94 | lines = data.splitlines()
95 | # NB: Use list comprehension and not a generator so we run all the checks.
96 | return all(
97 | [
98 | check_license(path, lines),
99 | ]
100 | )
101 |
102 |
103 | def check_paths(opts: argparse.Namespace, paths: list[Path]) -> bool:
104 | """Check all the paths."""
105 | # NB: Use list comprehension and not a generator so we check all paths.
106 | return all([check_path(opts, x) for x in paths])
107 |
108 |
109 | def find_files(opts: argparse.Namespace) -> list[Path]:
110 | """Find all the files in the source tree."""
111 | result = util.run(
112 | opts,
113 | ["git", "ls-tree", "-r", "-z", "--name-only", "HEAD"],
114 | cwd=util.TOPDIR,
115 | capture_output=True,
116 | encoding="utf-8",
117 | )
118 | return [util.TOPDIR / x for x in result.stdout.split("\0")[:-1]]
119 |
120 |
121 | def get_parser() -> argparse.ArgumentParser:
122 | """Get a CLI parser."""
123 | parser = argparse.ArgumentParser(description=__doc__)
124 | parser.add_argument(
125 | "-n",
126 | "--dry-run",
127 | dest="dryrun",
128 | action="store_true",
129 | help="show everything that would be done",
130 | )
131 | parser.add_argument(
132 | "paths",
133 | nargs="*",
134 | help="the paths to scan",
135 | )
136 | return parser
137 |
138 |
139 | def main(argv: list[str]) -> int:
140 | """The main func!"""
141 | parser = get_parser()
142 | opts = parser.parse_args(argv)
143 |
144 | paths = opts.paths
145 | if not opts.paths:
146 | paths = find_files(opts)
147 |
148 | return 0 if check_paths(opts, paths) else 1
149 |
150 |
151 | if __name__ == "__main__":
152 | sys.exit(main(sys.argv[1:]))
153 |
--------------------------------------------------------------------------------
/run_tests:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # Copyright (C) 2019 The Android Open Source Project
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | """Wrapper to run linters and pytest with the right settings."""
17 |
18 | import functools
19 | import os
20 | import shlex
21 | import shutil
22 | import subprocess
23 | import sys
24 | from typing import List
25 |
26 |
27 | ROOT_DIR = os.path.dirname(os.path.realpath(__file__))
28 |
29 |
30 | def log_cmd(cmd: str, argv: list[str]) -> None:
31 | """Log a debug message to make history easier to track."""
32 | print("+", cmd, shlex.join(argv), file=sys.stderr)
33 |
34 |
35 | @functools.lru_cache()
36 | def is_ci() -> bool:
37 | """Whether we're running in our CI system."""
38 | return os.getenv("LUCI_CQ") == "yes"
39 |
40 |
41 | def run_pytest(argv: List[str]) -> int:
42 | """Returns the exit code from pytest."""
43 | if is_ci():
44 | argv = ["-m", "not skip_cq"] + argv
45 |
46 | log_cmd("pytest", argv)
47 | return subprocess.run(
48 | [sys.executable, "-m", "pytest"] + argv,
49 | check=False,
50 | cwd=ROOT_DIR,
51 | ).returncode
52 |
53 |
54 | def run_pytest_py38(argv: List[str]) -> int:
55 | """Returns the exit code from pytest under Python 3.8."""
56 | if is_ci():
57 | argv = ["-m", "not skip_cq"] + argv
58 |
59 | log_cmd("[vpython 3.8] pytest", argv)
60 | try:
61 | return subprocess.run(
62 | [
63 | "vpython3",
64 | "-vpython-spec",
65 | "run_tests.vpython3.8",
66 | "-m",
67 | "pytest",
68 | ]
69 | + argv,
70 | check=False,
71 | cwd=ROOT_DIR,
72 | ).returncode
73 | except FileNotFoundError:
74 | # Skip if the user doesn't have vpython from depot_tools.
75 | return 0
76 |
77 |
78 | def run_black():
79 | """Returns the exit code from black."""
80 | # Black by default only matches .py files. We have to list standalone
81 | # scripts manually.
82 | extra_programs = [
83 | "repo",
84 | "run_tests",
85 | "release/update-hooks",
86 | "release/update-manpages",
87 | ]
88 | argv = ["--diff", "--check", ROOT_DIR] + extra_programs
89 | log_cmd("black", argv)
90 | return subprocess.run(
91 | [sys.executable, "-m", "black"] + argv,
92 | check=False,
93 | cwd=ROOT_DIR,
94 | ).returncode
95 |
96 |
97 | def run_flake8():
98 | """Returns the exit code from flake8."""
99 | argv = [ROOT_DIR]
100 | log_cmd("flake8", argv)
101 | return subprocess.run(
102 | [sys.executable, "-m", "flake8"] + argv,
103 | check=False,
104 | cwd=ROOT_DIR,
105 | ).returncode
106 |
107 |
108 | def run_isort():
109 | """Returns the exit code from isort."""
110 | argv = ["--check", ROOT_DIR]
111 | log_cmd("isort", argv)
112 | return subprocess.run(
113 | [sys.executable, "-m", "isort"] + argv,
114 | check=False,
115 | cwd=ROOT_DIR,
116 | ).returncode
117 |
118 |
119 | def run_check_metadata():
120 | """Returns the exit code from check-metadata."""
121 | argv = []
122 | log_cmd("release/check-metadata.py", argv)
123 | return subprocess.run(
124 | [sys.executable, "release/check-metadata.py"] + argv,
125 | check=False,
126 | cwd=ROOT_DIR,
127 | ).returncode
128 |
129 |
130 | def run_update_manpages() -> int:
131 | """Returns the exit code from release/update-manpages."""
132 | # Allow this to fail on CI, but not local devs.
133 | if is_ci() and not shutil.which("help2man"):
134 | print("update-manpages: help2man not found; skipping test")
135 | return 0
136 |
137 | argv = ["--check"]
138 | log_cmd("release/update-manpages", argv)
139 | return subprocess.run(
140 | [sys.executable, "release/update-manpages"] + argv,
141 | check=False,
142 | cwd=ROOT_DIR,
143 | ).returncode
144 |
145 |
146 | def main(argv):
147 | """The main entry."""
148 | checks = (
149 | functools.partial(run_pytest, argv),
150 | functools.partial(run_pytest_py38, argv),
151 | run_black,
152 | run_flake8,
153 | run_isort,
154 | run_check_metadata,
155 | run_update_manpages,
156 | )
157 | # Run all the tests all the time to get full feedback. Don't exit on the
158 | # first error as that makes it more difficult to iterate in the CQ.
159 | return 1 if sum(c() for c in checks) else 0
160 |
161 |
162 | if __name__ == "__main__":
163 | sys.exit(main(sys.argv[1:]))
164 |
--------------------------------------------------------------------------------
/man/repo-smartsync.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "August 2025" "repo smartsync" "Repo Manual"
3 | .SH NAME
4 | repo \- repo smartsync - manual page for repo smartsync
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,smartsync \/\fR[\fI\,\/\fR...]
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | Update working tree to the latest known good revision
12 | .SH OPTIONS
13 | .TP
14 | \fB\-h\fR, \fB\-\-help\fR
15 | show this help message and exit
16 | .TP
17 | \fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
18 | number of jobs to run in parallel (default: based on
19 | number of CPU cores)
20 | .TP
21 | \fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
22 | number of network jobs to run in parallel (defaults to
23 | \fB\-\-jobs\fR or 1). Ignored unless \fB\-\-no\-interleaved\fR is set
24 | .TP
25 | \fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
26 | number of local checkout jobs to run in parallel
27 | (defaults to \fB\-\-jobs\fR or 8). Ignored unless \fB\-\-nointerleaved\fR is set
28 | .TP
29 | \fB\-f\fR, \fB\-\-force\-broken\fR
30 | obsolete option (to be deleted in the future)
31 | .TP
32 | \fB\-\-fail\-fast\fR
33 | stop syncing after first error is hit
34 | .TP
35 | \fB\-\-force\-sync\fR
36 | overwrite an existing git directory if it needs to
37 | point to a different object directory. WARNING: this
38 | may cause loss of data
39 | .TP
40 | \fB\-\-force\-checkout\fR
41 | force checkout even if it results in throwing away
42 | uncommitted modifications. WARNING: this may cause
43 | loss of data
44 | .TP
45 | \fB\-\-force\-remove\-dirty\fR
46 | force remove projects with uncommitted modifications
47 | if projects no longer exist in the manifest. WARNING:
48 | this may cause loss of data
49 | .TP
50 | \fB\-\-rebase\fR
51 | rebase local commits regardless of whether they are
52 | published
53 | .TP
54 | \fB\-l\fR, \fB\-\-local\-only\fR
55 | only update working tree, don't fetch
56 | .TP
57 | \fB\-\-no\-manifest\-update\fR, \fB\-\-nmu\fR
58 | use the existing manifest checkout as\-is. (do not
59 | update to the latest revision)
60 | .TP
61 | \fB\-\-interleaved\fR
62 | fetch and checkout projects in parallel (default)
63 | .TP
64 | \fB\-\-no\-interleaved\fR
65 | fetch and checkout projects in phases
66 | .TP
67 | \fB\-n\fR, \fB\-\-network\-only\fR
68 | fetch only, don't update working tree
69 | .TP
70 | \fB\-d\fR, \fB\-\-detach\fR
71 | detach projects back to manifest revision
72 | .TP
73 | \fB\-c\fR, \fB\-\-current\-branch\fR
74 | fetch only current branch from server
75 | .TP
76 | \fB\-\-no\-current\-branch\fR
77 | fetch all branches from server
78 | .TP
79 | \fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
80 | temporary manifest to use for this sync
81 | .TP
82 | \fB\-\-clone\-bundle\fR
83 | enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
84 | .TP
85 | \fB\-\-no\-clone\-bundle\fR
86 | disable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
87 | .TP
88 | \fB\-u\fR MANIFEST_SERVER_USERNAME, \fB\-\-manifest\-server\-username\fR=\fI\,MANIFEST_SERVER_USERNAME\/\fR
89 | username to authenticate with the manifest server
90 | .TP
91 | \fB\-p\fR MANIFEST_SERVER_PASSWORD, \fB\-\-manifest\-server\-password\fR=\fI\,MANIFEST_SERVER_PASSWORD\/\fR
92 | password to authenticate with the manifest server
93 | .TP
94 | \fB\-\-fetch\-submodules\fR
95 | fetch submodules from server
96 | .TP
97 | \fB\-\-use\-superproject\fR
98 | use the manifest superproject to sync projects;
99 | implies \fB\-c\fR
100 | .TP
101 | \fB\-\-no\-use\-superproject\fR
102 | disable use of manifest superprojects
103 | .TP
104 | \fB\-\-tags\fR
105 | fetch tags
106 | .TP
107 | \fB\-\-no\-tags\fR
108 | don't fetch tags (default)
109 | .TP
110 | \fB\-\-optimized\-fetch\fR
111 | only fetch projects fixed to sha1 if revision does not
112 | exist locally
113 | .TP
114 | \fB\-\-retry\-fetches\fR=\fI\,RETRY_FETCHES\/\fR
115 | number of times to retry fetches on transient errors
116 | .TP
117 | \fB\-\-prune\fR
118 | delete refs that no longer exist on the remote
119 | (default)
120 | .TP
121 | \fB\-\-no\-prune\fR
122 | do not delete refs that no longer exist on the remote
123 | .TP
124 | \fB\-\-auto\-gc\fR
125 | run garbage collection on all synced projects
126 | .TP
127 | \fB\-\-no\-auto\-gc\fR
128 | do not run garbage collection on any projects
129 | (default)
130 | .SS Logging options:
131 | .TP
132 | \fB\-v\fR, \fB\-\-verbose\fR
133 | show all output
134 | .TP
135 | \fB\-q\fR, \fB\-\-quiet\fR
136 | only show errors
137 | .SS Multi\-manifest options:
138 | .TP
139 | \fB\-\-outer\-manifest\fR
140 | operate starting at the outermost manifest
141 | .TP
142 | \fB\-\-no\-outer\-manifest\fR
143 | do not operate on outer manifests
144 | .TP
145 | \fB\-\-this\-manifest\-only\fR
146 | only operate on this (sub)manifest
147 | .TP
148 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
149 | operate on this manifest and its submanifests
150 | .SS repo Version options:
151 | .TP
152 | \fB\-\-no\-repo\-verify\fR
153 | do not verify repo source code
154 | .SS post\-sync hooks:
155 | .TP
156 | \fB\-\-no\-verify\fR
157 | Do not run the post\-sync hook.
158 | .TP
159 | \fB\-\-verify\fR
160 | Run the post\-sync hook without prompting.
161 | .TP
162 | \fB\-\-ignore\-hooks\fR
163 | Do not abort if post\-sync hooks fail.
164 | .PP
165 | Run `repo help smartsync` to view the detailed manual.
166 | .SH DETAILS
167 | .PP
168 | The 'repo smartsync' command is a shortcut for sync \fB\-s\fR.
169 |
--------------------------------------------------------------------------------
/release/sign-tag.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # Copyright (C) 2020 The Android Open Source Project
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | """Helper tool for signing repo release tags correctly.
17 |
18 | This is intended to be run only by the official Repo release managers, but it
19 | could be run by people maintaining their own fork of the project.
20 |
21 | NB: Check docs/release-process.md for production freeze information.
22 | """
23 |
24 | import argparse
25 | import os
26 | import re
27 | import subprocess
28 | import sys
29 |
30 | import util
31 |
32 |
33 | # We currently sign with the old DSA key as it's been around the longest.
34 | # We should transition to RSA by Jun 2020, and ECC by Jun 2021.
35 | KEYID = util.KEYID_DSA
36 |
37 | # Regular expression to validate tag names.
38 | RE_VALID_TAG = r"^v([0-9]+[.])+[0-9]+$"
39 |
40 |
41 | def sign(opts):
42 | """Tag the commit & sign it!"""
43 | # We use ! at the end of the key so that gpg uses this specific key.
44 | # Otherwise it uses the key as a lookup into the overall key and uses the
45 | # default signing key. i.e. It will see that KEYID_RSA is a subkey of
46 | # another key, and use the primary key to sign instead of the subkey.
47 | cmd = [
48 | "git",
49 | "tag",
50 | "-s",
51 | opts.tag,
52 | "-u",
53 | f"{opts.key}!",
54 | "-m",
55 | f"repo {opts.tag}",
56 | opts.commit,
57 | ]
58 |
59 | key = "GNUPGHOME"
60 | print("+", f'export {key}="{opts.gpgdir}"')
61 | oldvalue = os.getenv(key)
62 | os.putenv(key, opts.gpgdir)
63 | util.run(opts, cmd)
64 | if oldvalue is None:
65 | os.unsetenv(key)
66 | else:
67 | os.putenv(key, oldvalue)
68 |
69 |
70 | def check(opts):
71 | """Check the signature."""
72 | util.run(opts, ["git", "tag", "--verify", opts.tag])
73 |
74 |
75 | def postmsg(opts):
76 | """Helpful info to show at the end for release manager."""
77 | cmd = ["git", "rev-parse", "remotes/origin/stable"]
78 | ret = util.run(opts, cmd, encoding="utf-8", stdout=subprocess.PIPE)
79 | current_release = ret.stdout.strip()
80 |
81 | cmd = [
82 | "git",
83 | "log",
84 | "--format=%h (%aN) %s",
85 | "--no-merges",
86 | f"remotes/origin/stable..{opts.tag}",
87 | ]
88 | ret = util.run(opts, cmd, encoding="utf-8", stdout=subprocess.PIPE)
89 | shortlog = ret.stdout.strip()
90 |
91 | print(
92 | f"""
93 | Here's the short log since the last release.
94 | {shortlog}
95 |
96 | To push release to the public:
97 | git push origin {opts.commit}:stable {opts.tag} -n
98 | NB: People will start upgrading to this version immediately.
99 |
100 | To roll back a release:
101 | git push origin --force {current_release}:stable -n
102 | """
103 | )
104 |
105 |
106 | def get_parser():
107 | """Get a CLI parser."""
108 | parser = argparse.ArgumentParser(
109 | description=__doc__,
110 | formatter_class=argparse.RawDescriptionHelpFormatter,
111 | )
112 | parser.add_argument(
113 | "-n",
114 | "--dry-run",
115 | dest="dryrun",
116 | action="store_true",
117 | help="show everything that would be done",
118 | )
119 | parser.add_argument(
120 | "--gpgdir",
121 | default=os.path.join(util.HOMEDIR, ".gnupg", "repo"),
122 | help="path to dedicated gpg dir with release keys "
123 | "(default: ~/.gnupg/repo/)",
124 | )
125 | parser.add_argument(
126 | "-f", "--force", action="store_true", help="force signing of any tag"
127 | )
128 | parser.add_argument(
129 | "--keyid", dest="key", help="alternative signing key to use"
130 | )
131 | parser.add_argument("tag", help='the tag to create (e.g. "v2.0")')
132 | parser.add_argument(
133 | "commit", default="HEAD", nargs="?", help="the commit to tag"
134 | )
135 | return parser
136 |
137 |
138 | def main(argv):
139 | """The main func!"""
140 | parser = get_parser()
141 | opts = parser.parse_args(argv)
142 |
143 | if not os.path.exists(opts.gpgdir):
144 | parser.error(f"--gpgdir does not exist: {opts.gpgdir}")
145 |
146 | if not opts.force and not re.match(RE_VALID_TAG, opts.tag):
147 | parser.error(
148 | f'tag "{opts.tag}" does not match regex "{RE_VALID_TAG}"; '
149 | "use --force to sign anyways"
150 | )
151 |
152 | if opts.key:
153 | print(f"Using custom key to sign: {opts.key}")
154 | else:
155 | print("Using official Repo release key to sign")
156 | opts.key = KEYID
157 | util.import_release_key(opts)
158 |
159 | sign(opts)
160 | check(opts)
161 | postmsg(opts)
162 |
163 | return 0
164 |
165 |
166 | if __name__ == "__main__":
167 | sys.exit(main(sys.argv[1:]))
168 |
--------------------------------------------------------------------------------
/man/repo-forall.1:
--------------------------------------------------------------------------------
1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2 | .TH REPO "1" "July 2022" "repo forall" "Repo Manual"
3 | .SH NAME
4 | repo \- repo forall - manual page for repo forall
5 | .SH SYNOPSIS
6 | .B repo
7 | \fI\,forall \/\fR[\fI\,\/\fR...] \fI\,-c \/\fR[\fI\,\/\fR...]
8 | .SH DESCRIPTION
9 | Summary
10 | .PP
11 | Run a shell command in each project
12 | .PP
13 | repo forall \fB\-r\fR str1 [str2] ... \fB\-c\fR [...]
14 | .SH OPTIONS
15 | .TP
16 | \fB\-h\fR, \fB\-\-help\fR
17 | show this help message and exit
18 | .TP
19 | \fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
20 | number of jobs to run in parallel (default: based on
21 | number of CPU cores)
22 | .TP
23 | \fB\-r\fR, \fB\-\-regex\fR
24 | execute the command only on projects matching regex or
25 | wildcard expression
26 | .TP
27 | \fB\-i\fR, \fB\-\-inverse\-regex\fR
28 | execute the command only on projects not matching
29 | regex or wildcard expression
30 | .TP
31 | \fB\-g\fR GROUPS, \fB\-\-groups\fR=\fI\,GROUPS\/\fR
32 | execute the command only on projects matching the
33 | specified groups
34 | .TP
35 | \fB\-c\fR, \fB\-\-command\fR
36 | command (and arguments) to execute
37 | .TP
38 | \fB\-e\fR, \fB\-\-abort\-on\-errors\fR
39 | abort if a command exits unsuccessfully
40 | .TP
41 | \fB\-\-ignore\-missing\fR
42 | silently skip & do not exit non\-zero due missing
43 | checkouts
44 | .TP
45 | \fB\-\-interactive\fR
46 | force interactive usage
47 | .SS Logging options:
48 | .TP
49 | \fB\-v\fR, \fB\-\-verbose\fR
50 | show all output
51 | .TP
52 | \fB\-q\fR, \fB\-\-quiet\fR
53 | only show errors
54 | .TP
55 | \fB\-p\fR
56 | show project headers before output
57 | .SS Multi\-manifest options:
58 | .TP
59 | \fB\-\-outer\-manifest\fR
60 | operate starting at the outermost manifest
61 | .TP
62 | \fB\-\-no\-outer\-manifest\fR
63 | do not operate on outer manifests
64 | .TP
65 | \fB\-\-this\-manifest\-only\fR
66 | only operate on this (sub)manifest
67 | .TP
68 | \fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
69 | operate on this manifest and its submanifests
70 | .PP
71 | Run `repo help forall` to view the detailed manual.
72 | .SH DETAILS
73 | .PP
74 | Executes the same shell command in each project.
75 | .PP
76 | The \fB\-r\fR option allows running the command only on projects matching regex or
77 | wildcard expression.
78 | .PP
79 | By default, projects are processed non\-interactively in parallel. If you want to
80 | run interactive commands, make sure to pass \fB\-\-interactive\fR to force \fB\-\-jobs\fR 1.
81 | While the processing order of projects is not guaranteed, the order of project
82 | output is stable.
83 | .PP
84 | Output Formatting
85 | .PP
86 | The \fB\-p\fR option causes 'repo forall' to bind pipes to the command's stdin, stdout
87 | and stderr streams, and pipe all output into a continuous stream that is
88 | displayed in a single pager session. Project headings are inserted before the
89 | output of each command is displayed. If the command produces no output in a
90 | project, no heading is displayed.
91 | .PP
92 | The formatting convention used by \fB\-p\fR is very suitable for some types of
93 | searching, e.g. `repo forall \fB\-p\fR \fB\-c\fR git log \fB\-SFoo\fR` will print all commits that
94 | add or remove references to Foo.
95 | .PP
96 | The \fB\-v\fR option causes 'repo forall' to display stderr messages if a command
97 | produces output only on stderr. Normally the \fB\-p\fR option causes command output to
98 | be suppressed until the command produces at least one byte of output on stdout.
99 | .PP
100 | Environment
101 | .PP
102 | pwd is the project's working directory. If the current client is a mirror
103 | client, then pwd is the Git repository.
104 | .PP
105 | REPO_PROJECT is set to the unique name of the project.
106 | .PP
107 | REPO_PATH is the path relative the the root of the client.
108 | .PP
109 | REPO_OUTERPATH is the path of the sub manifest's root relative to the root of
110 | the client.
111 | .PP
112 | REPO_INNERPATH is the path relative to the root of the sub manifest.
113 | .PP
114 | REPO_REMOTE is the name of the remote system from the manifest.
115 | .PP
116 | REPO_LREV is the name of the revision from the manifest, translated to a local
117 | tracking branch. If you need to pass the manifest revision to a locally executed
118 | git command, use REPO_LREV.
119 | .PP
120 | REPO_RREV is the name of the revision from the manifest, exactly as written in
121 | the manifest.
122 | .PP
123 | REPO_COUNT is the total number of projects being iterated.
124 | .PP
125 | REPO_I is the current (1\-based) iteration count. Can be used in conjunction with
126 | REPO_COUNT to add a simple progress indicator to your command.
127 | .PP
128 | REPO__* are any extra environment variables, specified by the "annotation"
129 | element under any project element. This can be useful for differentiating trees
130 | based on user\-specific criteria, or simply annotating tree details.
131 | .PP
132 | shell positional arguments ($1, $2, .., $#) are set to any arguments following
133 | .
134 | .PP
135 | Example: to list projects:
136 | .IP
137 | repo forall \fB\-c\fR 'echo $REPO_PROJECT'
138 | .PP
139 | Notice that $REPO_PROJECT is quoted to ensure it is expanded in the context of
140 | running instead of in the calling shell.
141 | .PP
142 | Unless \fB\-p\fR is used, stdin, stdout, stderr are inherited from the terminal and are
143 | not redirected.
144 | .PP
145 | If \fB\-e\fR is used, when a command exits unsuccessfully, 'repo forall' will abort
146 | without iterating through the remaining projects.
147 |
--------------------------------------------------------------------------------
/tests/test_subcmds_manifest.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2025 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Unittests for the subcmds/manifest.py module."""
16 |
17 | import json
18 | from pathlib import Path
19 | from unittest import mock
20 |
21 | import manifest_xml
22 | from subcmds import manifest
23 |
24 |
25 | _EXAMPLE_MANIFEST = """\
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | """
34 |
35 |
36 | def _get_cmd(repodir: Path) -> manifest.Manifest:
37 | """Instantiate a manifest command object to test."""
38 | manifests_git = repodir / "manifests.git"
39 | manifests_git.mkdir()
40 | (manifests_git / "config").write_text(
41 | """
42 | [remote "origin"]
43 | \turl = http://localhost/manifest
44 | """
45 | )
46 | client = manifest_xml.RepoClient(repodir=str(repodir))
47 | git_event_log = mock.MagicMock(ErrorEvent=mock.Mock(return_value=None))
48 | return manifest.Manifest(
49 | repodir=client.repodir,
50 | client=client,
51 | manifest=client.manifest,
52 | outer_client=client,
53 | outer_manifest=client.manifest,
54 | git_event_log=git_event_log,
55 | )
56 |
57 |
58 | def test_output_format_xml_file(tmp_path):
59 | """Test writing XML to a file."""
60 | path = tmp_path / "manifest.xml"
61 | path.write_text(_EXAMPLE_MANIFEST)
62 | outpath = tmp_path / "output.xml"
63 | cmd = _get_cmd(tmp_path)
64 | opt, args = cmd.OptionParser.parse_args(["--output-file", str(outpath)])
65 | cmd.Execute(opt, args)
66 | # Normalize the output a bit as we don't exactly care.
67 | normalize = lambda data: "\n".join(
68 | x.strip() for x in data.splitlines() if x.strip()
69 | )
70 | assert (
71 | normalize(outpath.read_text())
72 | == """
73 |
74 |
75 |
76 |
77 |
78 | """
79 | )
80 |
81 |
82 | def test_output_format_xml_stdout(tmp_path, capsys):
83 | """Test writing XML to stdout."""
84 | path = tmp_path / "manifest.xml"
85 | path.write_text(_EXAMPLE_MANIFEST)
86 | cmd = _get_cmd(tmp_path)
87 | opt, args = cmd.OptionParser.parse_args(["--format", "xml"])
88 | cmd.Execute(opt, args)
89 | # Normalize the output a bit as we don't exactly care.
90 | normalize = lambda data: "\n".join(
91 | x.strip() for x in data.splitlines() if x.strip()
92 | )
93 | stdout = capsys.readouterr().out
94 | assert (
95 | normalize(stdout)
96 | == """
97 |
98 |
99 |
100 |
101 |
102 | """
103 | )
104 |
105 |
106 | def test_output_format_json(tmp_path, capsys):
107 | """Test writing JSON."""
108 | path = tmp_path / "manifest.xml"
109 | path.write_text(_EXAMPLE_MANIFEST)
110 | cmd = _get_cmd(tmp_path)
111 | opt, args = cmd.OptionParser.parse_args(["--format", "json"])
112 | cmd.Execute(opt, args)
113 | obj = json.loads(capsys.readouterr().out)
114 | assert obj == {
115 | "default": {"remote": "test-remote", "revision": "refs/heads/main"},
116 | "project": [{"name": "repohooks", "path": "src/repohooks"}],
117 | "remote": [{"fetch": "http://localhost", "name": "test-remote"}],
118 | "repo-hooks": {"enabled-list": "a b", "in-project": "repohooks"},
119 | }
120 |
121 |
122 | def test_output_format_json_pretty(tmp_path, capsys):
123 | """Test writing pretty JSON."""
124 | path = tmp_path / "manifest.xml"
125 | path.write_text(_EXAMPLE_MANIFEST)
126 | cmd = _get_cmd(tmp_path)
127 | opt, args = cmd.OptionParser.parse_args(["--format", "json", "--pretty"])
128 | cmd.Execute(opt, args)
129 | stdout = capsys.readouterr().out
130 | assert (
131 | stdout
132 | == """\
133 | {
134 | "default": {
135 | "remote": "test-remote",
136 | "revision": "refs/heads/main"
137 | },
138 | "project": [
139 | {
140 | "name": "repohooks",
141 | "path": "src/repohooks"
142 | }
143 | ],
144 | "remote": [
145 | {
146 | "fetch": "http://localhost",
147 | "name": "test-remote"
148 | }
149 | ],
150 | "repo-hooks": {
151 | "enabled-list": "a b",
152 | "in-project": "repohooks"
153 | }
154 | }
155 | """
156 | )
157 |
--------------------------------------------------------------------------------
/git_refs.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2009 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import os
16 |
17 | import platform_utils
18 | from repo_trace import Trace
19 |
20 |
21 | HEAD = "HEAD"
22 | R_CHANGES = "refs/changes/"
23 | R_HEADS = "refs/heads/"
24 | R_TAGS = "refs/tags/"
25 | R_PUB = "refs/published/"
26 | R_WORKTREE = "refs/worktree/"
27 | R_WORKTREE_M = R_WORKTREE + "m/"
28 | R_M = "refs/remotes/m/"
29 |
30 |
31 | class GitRefs:
32 | def __init__(self, gitdir):
33 | self._gitdir = gitdir
34 | self._phyref = None
35 | self._symref = None
36 | self._mtime = {}
37 |
38 | @property
39 | def all(self):
40 | self._EnsureLoaded()
41 | return self._phyref
42 |
43 | def get(self, name):
44 | try:
45 | return self.all[name]
46 | except KeyError:
47 | return ""
48 |
49 | def deleted(self, name):
50 | if self._phyref is not None:
51 | if name in self._phyref:
52 | del self._phyref[name]
53 |
54 | if name in self._symref:
55 | del self._symref[name]
56 |
57 | if name in self._mtime:
58 | del self._mtime[name]
59 |
60 | def symref(self, name):
61 | try:
62 | self._EnsureLoaded()
63 | return self._symref[name]
64 | except KeyError:
65 | return ""
66 |
67 | def _EnsureLoaded(self):
68 | if self._phyref is None or self._NeedUpdate():
69 | self._LoadAll()
70 |
71 | def _NeedUpdate(self):
72 | with Trace(": scan refs %s", self._gitdir):
73 | for name, mtime in self._mtime.items():
74 | try:
75 | if mtime != os.path.getmtime(
76 | os.path.join(self._gitdir, name)
77 | ):
78 | return True
79 | except OSError:
80 | return True
81 | return False
82 |
83 | def _LoadAll(self):
84 | with Trace(": load refs %s", self._gitdir):
85 | self._phyref = {}
86 | self._symref = {}
87 | self._mtime = {}
88 |
89 | self._ReadPackedRefs()
90 | self._ReadLoose("refs/")
91 | self._ReadLoose1(os.path.join(self._gitdir, HEAD), HEAD)
92 |
93 | scan = self._symref
94 | attempts = 0
95 | while scan and attempts < 5:
96 | scan_next = {}
97 | for name, dest in scan.items():
98 | if dest in self._phyref:
99 | self._phyref[name] = self._phyref[dest]
100 | else:
101 | scan_next[name] = dest
102 | scan = scan_next
103 | attempts += 1
104 |
105 | def _ReadPackedRefs(self):
106 | path = os.path.join(self._gitdir, "packed-refs")
107 | try:
108 | fd = open(path)
109 | mtime = os.path.getmtime(path)
110 | except OSError:
111 | return
112 | try:
113 | for line in fd:
114 | line = str(line)
115 | if line[0] == "#":
116 | continue
117 | if line[0] == "^":
118 | continue
119 |
120 | line = line[:-1]
121 | p = line.split(" ")
122 | ref_id = p[0]
123 | name = p[1]
124 |
125 | self._phyref[name] = ref_id
126 | finally:
127 | fd.close()
128 | self._mtime["packed-refs"] = mtime
129 |
130 | def _ReadLoose(self, prefix):
131 | base = os.path.join(self._gitdir, prefix)
132 | for name in platform_utils.listdir(base):
133 | p = os.path.join(base, name)
134 | # We don't implement the full ref validation algorithm, just the
135 | # simple rules that would show up in local filesystems.
136 | # https://git-scm.com/docs/git-check-ref-format
137 | if name.startswith(".") or name.endswith(".lock"):
138 | pass
139 | elif platform_utils.isdir(p):
140 | self._mtime[prefix] = os.path.getmtime(base)
141 | self._ReadLoose(prefix + name + "/")
142 | else:
143 | self._ReadLoose1(p, prefix + name)
144 |
145 | def _ReadLoose1(self, path, name):
146 | try:
147 | with open(path) as fd:
148 | mtime = os.path.getmtime(path)
149 | ref_id = fd.readline()
150 | except (OSError, UnicodeError):
151 | return
152 |
153 | try:
154 | ref_id = ref_id.decode()
155 | except AttributeError:
156 | pass
157 | if not ref_id:
158 | return
159 | ref_id = ref_id[:-1]
160 |
161 | if ref_id.startswith("ref: "):
162 | self._symref[name] = ref_id[5:]
163 | else:
164 | self._phyref[name] = ref_id
165 | self._mtime[name] = mtime
166 |
--------------------------------------------------------------------------------
/subcmds/start.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2008 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import functools
16 | from typing import NamedTuple
17 |
18 | from command import Command
19 | from command import DEFAULT_LOCAL_JOBS
20 | from error import RepoExitError
21 | from git_command import git
22 | from git_config import IsImmutable
23 | from progress import Progress
24 | from repo_logging import RepoLogger
25 |
26 |
27 | logger = RepoLogger(__file__)
28 |
29 |
30 | class ExecuteOneResult(NamedTuple):
31 | project_idx: int
32 | error: Exception
33 |
34 |
35 | class StartError(RepoExitError):
36 | """Exit error for failed start command."""
37 |
38 |
39 | class Start(Command):
40 | COMMON = True
41 | helpSummary = "Start a new branch for development"
42 | helpUsage = """
43 | %prog [--all | ...]
44 | """
45 | helpDescription = """
46 | '%prog' begins a new branch of development, starting from the
47 | revision specified in the manifest.
48 | """
49 | PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
50 |
51 | def _Options(self, p):
52 | p.add_option(
53 | "--all",
54 | action="store_true",
55 | help="begin branch in all projects",
56 | )
57 | p.add_option(
58 | "-r",
59 | "--rev",
60 | "--revision",
61 | dest="revision",
62 | help="point branch at this revision instead of upstream",
63 | )
64 | p.add_option(
65 | "--head",
66 | "--HEAD",
67 | dest="revision",
68 | action="store_const",
69 | const="HEAD",
70 | help="abbreviation for --rev HEAD",
71 | )
72 |
73 | def ValidateOptions(self, opt, args):
74 | if not args:
75 | self.Usage()
76 |
77 | nb = args[0]
78 | if not git.check_ref_format("heads/%s" % nb):
79 | self.OptionParser.error("'%s' is not a valid name" % nb)
80 |
81 | @classmethod
82 | def _ExecuteOne(cls, revision, nb, default_revisionExpr, project_idx):
83 | """Start one project."""
84 | # If the current revision is immutable, such as a SHA1, a tag or
85 | # a change, then we can't push back to it. Substitute with
86 | # dest_branch, if defined; or with manifest default revision instead.
87 | branch_merge = ""
88 | error = None
89 | project = cls.get_parallel_context()["projects"][project_idx]
90 | if IsImmutable(project.revisionExpr):
91 | if project.dest_branch:
92 | branch_merge = project.dest_branch
93 | else:
94 | branch_merge = default_revisionExpr
95 |
96 | try:
97 | project.StartBranch(
98 | nb, branch_merge=branch_merge, revision=revision
99 | )
100 | except Exception as e:
101 | logger.error("error: unable to checkout %s: %s", project.name, e)
102 | error = e
103 | return ExecuteOneResult(project_idx, error)
104 |
105 | def Execute(self, opt, args):
106 | nb = args[0]
107 | err_projects = []
108 | err = []
109 | projects = []
110 | if not opt.all:
111 | projects = args[1:]
112 | if len(projects) < 1:
113 | projects = ["."] # start it in the local project by default
114 |
115 | all_projects = self.GetProjects(
116 | projects,
117 | all_manifests=not opt.this_manifest_only,
118 | )
119 |
120 | def _ProcessResults(_pool, pm, results):
121 | for result in results:
122 | if result.error:
123 | project = all_projects[result.project_idx]
124 | err_projects.append(project)
125 | err.append(result.error)
126 | pm.update(msg="")
127 |
128 | with self.ParallelContext():
129 | self.get_parallel_context()["projects"] = all_projects
130 | self.ExecuteInParallel(
131 | opt.jobs,
132 | functools.partial(
133 | self._ExecuteOne,
134 | opt.revision,
135 | nb,
136 | self.manifest.default.revisionExpr,
137 | ),
138 | range(len(all_projects)),
139 | callback=_ProcessResults,
140 | output=Progress(
141 | f"Starting {nb}", len(all_projects), quiet=opt.quiet
142 | ),
143 | chunksize=1,
144 | )
145 |
146 | if err_projects:
147 | for p in err_projects:
148 | logger.error(
149 | "error: %s/: cannot start %s",
150 | p.RelPath(local=opt.this_manifest_only),
151 | nb,
152 | )
153 | msg_fmt = "cannot start %d project(s)"
154 | self.git_event_log.ErrorEvent(
155 | msg_fmt % (len(err_projects)), msg_fmt
156 | )
157 | raise StartError(aggregate_errors=err)
158 |
--------------------------------------------------------------------------------
/repo_trace.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2008 The Android Open Source Project
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Logic for tracing repo interactions.
16 |
17 | Activated via `repo --trace ...` or `REPO_TRACE=1 repo ...`.
18 |
19 | Temporary: Tracing is always on. Set `REPO_TRACE=0` to turn off.
20 | To also include trace outputs in stderr do `repo --trace_to_stderr ...`
21 | """
22 |
23 | import contextlib
24 | import os
25 | import sys
26 | import tempfile
27 | import time
28 |
29 | import platform_utils
30 |
31 |
32 | # Env var to implicitly turn on tracing.
33 | REPO_TRACE = "REPO_TRACE"
34 |
35 | # Temporarily set tracing to always on unless user expicitly sets to 0.
36 | _TRACE = os.environ.get(REPO_TRACE) != "0"
37 | _TRACE_TO_STDERR = False
38 | _TRACE_FILE = None
39 | _TRACE_FILE_NAME = "TRACE_FILE"
40 | _MAX_SIZE = 70 # in MiB
41 | _NEW_COMMAND_SEP = "+++++++++++++++NEW COMMAND+++++++++++++++++++"
42 |
43 |
44 | def IsTraceToStderr():
45 | """Whether traces are written to stderr."""
46 | return _TRACE_TO_STDERR
47 |
48 |
49 | def IsTrace():
50 | """Whether tracing is enabled."""
51 | return _TRACE
52 |
53 |
54 | def SetTraceToStderr():
55 | """Enables tracing logging to stderr."""
56 | global _TRACE_TO_STDERR
57 | _TRACE_TO_STDERR = True
58 |
59 |
60 | def SetTrace():
61 | """Enables tracing."""
62 | global _TRACE
63 | _TRACE = True
64 |
65 |
66 | def _SetTraceFile(quiet):
67 | """Sets the trace file location."""
68 | global _TRACE_FILE
69 | _TRACE_FILE = _GetTraceFile(quiet)
70 |
71 |
72 | class Trace(contextlib.ContextDecorator):
73 | """Used to capture and save git traces."""
74 |
75 | def _time(self):
76 | """Generate nanoseconds of time in a py3.6 safe way"""
77 | return int(time.time() * 1e9)
78 |
79 | def __init__(self, fmt, *args, first_trace=False, quiet=True):
80 | """Initialize the object.
81 |
82 | Args:
83 | fmt: The format string for the trace.
84 | *args: Arguments to pass to formatting.
85 | first_trace: Whether this is the first trace of a `repo` invocation.
86 | quiet: Whether to suppress notification of trace file location.
87 | """
88 | if not IsTrace():
89 | return
90 | self._trace_msg = fmt % args
91 |
92 | if not _TRACE_FILE:
93 | _SetTraceFile(quiet)
94 |
95 | if first_trace:
96 | _ClearOldTraces()
97 | self._trace_msg = f"{_NEW_COMMAND_SEP} {self._trace_msg}"
98 |
99 | def __enter__(self):
100 | if not IsTrace():
101 | return self
102 |
103 | print_msg = (
104 | f"PID: {os.getpid()} START: {self._time()} :{self._trace_msg}\n"
105 | )
106 |
107 | with open(_TRACE_FILE, "a") as f:
108 | print(print_msg, file=f)
109 |
110 | if _TRACE_TO_STDERR:
111 | print(print_msg, file=sys.stderr)
112 |
113 | return self
114 |
115 | def __exit__(self, *exc):
116 | if not IsTrace():
117 | return False
118 |
119 | print_msg = (
120 | f"PID: {os.getpid()} END: {self._time()} :{self._trace_msg}\n"
121 | )
122 |
123 | with open(_TRACE_FILE, "a") as f:
124 | print(print_msg, file=f)
125 |
126 | if _TRACE_TO_STDERR:
127 | print(print_msg, file=sys.stderr)
128 |
129 | return False
130 |
131 |
132 | def _GetTraceFile(quiet):
133 | """Get the trace file or create one."""
134 | # TODO: refactor to pass repodir to Trace.
135 | repo_dir = os.path.dirname(os.path.dirname(__file__))
136 | trace_file = os.path.join(repo_dir, _TRACE_FILE_NAME)
137 | if not quiet:
138 | print(f"Trace outputs in {trace_file}", file=sys.stderr)
139 | return trace_file
140 |
141 |
142 | def _ClearOldTraces():
143 | """Clear the oldest commands if trace file is too big."""
144 | try:
145 | with open(_TRACE_FILE, errors="ignore") as f:
146 | if os.path.getsize(f.name) / (1024 * 1024) <= _MAX_SIZE:
147 | return
148 | trace_lines = f.readlines()
149 | except FileNotFoundError:
150 | return
151 |
152 | while sum(len(x) for x in trace_lines) / (1024 * 1024) > _MAX_SIZE:
153 | for i, line in enumerate(trace_lines):
154 | if "END:" in line and _NEW_COMMAND_SEP in line:
155 | trace_lines = trace_lines[i + 1 :]
156 | break
157 | else:
158 | # The last chunk is bigger than _MAX_SIZE, so just throw everything
159 | # away.
160 | trace_lines = []
161 |
162 | while trace_lines and trace_lines[-1] == "\n":
163 | trace_lines = trace_lines[:-1]
164 | # Write to a temporary file with a unique name in the same filesystem
165 | # before replacing the original trace file.
166 | temp_dir, temp_prefix = os.path.split(_TRACE_FILE)
167 | with tempfile.NamedTemporaryFile(
168 | "w", dir=temp_dir, prefix=temp_prefix, delete=False
169 | ) as f:
170 | f.writelines(trace_lines)
171 | platform_utils.rename(f.name, _TRACE_FILE)
172 |
--------------------------------------------------------------------------------