summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.flake816
-rw-r--r--.github/workflows/test-ci.yml31
-rw-r--r--.gitignore2
-rw-r--r--.mailmap1
-rw-r--r--README.md29
-rw-r--r--SUBMITTING_PATCHES.md40
-rw-r--r--color.py3
-rw-r--r--command.py137
-rw-r--r--completion.bash156
-rw-r--r--docs/internal-fs-layout.md263
-rw-r--r--docs/manifest-format.md161
-rw-r--r--docs/python-support.md4
-rw-r--r--docs/release-process.md194
-rw-r--r--docs/repo-hooks.md2
-rw-r--r--docs/windows.md33
-rw-r--r--editor.py10
-rw-r--r--error.py43
-rw-r--r--event_log.py8
-rw-r--r--fetch.py41
-rw-r--r--git_command.py209
-rw-r--r--git_config.py426
-rw-r--r--git_refs.py25
-rw-r--r--git_superproject.py415
-rw-r--r--git_trace2_event_log.py273
-rw-r--r--gitc_utils.py101
-rw-r--r--hooks.py509
-rwxr-xr-xhooks/commit-msg218
-rwxr-xr-xmain.py302
-rw-r--r--man/repo-abandon.136
-rw-r--r--man/repo-branch.11
-rw-r--r--man/repo-branches.159
-rw-r--r--man/repo-checkout.136
-rw-r--r--man/repo-cherry-pick.128
-rw-r--r--man/repo-diff.135
-rw-r--r--man/repo-diffmanifests.161
-rw-r--r--man/repo-download.144
-rw-r--r--man/repo-forall.1128
-rw-r--r--man/repo-gitc-delete.131
-rw-r--r--man/repo-gitc-init.1150
-rw-r--r--man/repo-grep.1119
-rw-r--r--man/repo-help.133
-rw-r--r--man/repo-info.140
-rw-r--r--man/repo-init.1170
-rw-r--r--man/repo-list.161
-rw-r--r--man/repo-manifest.1548
-rw-r--r--man/repo-overview.139
-rw-r--r--man/repo-prune.128
-rw-r--r--man/repo-rebase.155
-rw-r--r--man/repo-selfupdate.135
-rw-r--r--man/repo-smartsync.1118
-rw-r--r--man/repo-stage.130
-rw-r--r--man/repo-start.141
-rw-r--r--man/repo-status.198
-rw-r--r--man/repo-sync.1209
-rw-r--r--man/repo-upload.1175
-rw-r--r--man/repo-version.124
-rw-r--r--man/repo.1133
-rw-r--r--manifest_xml.py888
-rw-r--r--pager.py16
-rw-r--r--platform_utils.py183
-rw-r--r--platform_utils_win32.py29
-rw-r--r--progress.py90
-rw-r--r--project.py1441
-rw-r--r--pyversion.py20
-rw-r--r--release/README.md2
-rwxr-xr-xrelease/sign-launcher.py114
-rwxr-xr-xrelease/sign-tag.py140
-rwxr-xr-xrelease/update-manpages102
-rw-r--r--release/util.py73
-rwxr-xr-xrepo1190
-rw-r--r--repo_trace.py6
-rw-r--r--requirements.json57
-rwxr-xr-xrun_tests50
-rwxr-xr-xsetup.py12
-rw-r--r--ssh.py277
-rw-r--r--subcmds/__init__.py11
-rw-r--r--subcmds/abandon.py75
-rw-r--r--subcmds/branches.py49
-rw-r--r--subcmds/checkout.py39
-rw-r--r--subcmds/cherry_pick.py27
-rw-r--r--subcmds/diff.py53
-rw-r--r--subcmds/diffmanifests.py32
-rw-r--r--subcmds/download.py80
-rw-r--r--subcmds/forall.py289
-rw-r--r--subcmds/gitc_delete.py10
-rw-r--r--subcmds/gitc_init.py19
-rw-r--r--subcmds/grep.py202
-rw-r--r--subcmds/help.py61
-rw-r--r--subcmds/info.py55
-rw-r--r--subcmds/init.py362
-rw-r--r--subcmds/list.py49
-rw-r--r--subcmds/manifest.py70
-rw-r--r--subcmds/overview.py20
-rw-r--r--subcmds/prune.py31
-rw-r--r--subcmds/rebase.py39
-rw-r--r--subcmds/selfupdate.py12
-rw-r--r--subcmds/smartsync.py5
-rw-r--r--subcmds/stage.py11
-rw-r--r--subcmds/start.py68
-rw-r--r--subcmds/status.py96
-rw-r--r--subcmds/sync.py1054
-rw-r--r--subcmds/upload.py315
-rw-r--r--subcmds/version.py27
-rw-r--r--tests/fixtures/test.gitconfig20
-rw-r--r--tests/test_editor.py4
-rw-r--r--tests/test_error.py53
-rw-r--r--tests/test_git_command.py54
-rw-r--r--tests/test_git_config.py134
-rw-r--r--tests/test_git_superproject.py376
-rw-r--r--tests/test_git_trace2_event_log.py329
-rw-r--r--tests/test_hooks.py55
-rw-r--r--tests/test_manifest_xml.py845
-rw-r--r--tests/test_platform_utils.py50
-rw-r--r--tests/test_project.py297
-rw-r--r--tests/test_ssh.py74
-rw-r--r--tests/test_subcmds.py73
-rw-r--r--tests/test_subcmds_init.py49
-rw-r--r--tests/test_wrapper.py511
-rw-r--r--tox.ini15
-rw-r--r--wrapper.py6
120 files changed, 13197 insertions, 3816 deletions
diff --git a/.flake8 b/.flake8
index 45ab6562..6b824e97 100644
--- a/.flake8
+++ b/.flake8
@@ -1,3 +1,15 @@
1[flake8] 1[flake8]
2max-line-length=80 2max-line-length=100
3ignore=E111,E114,E402 3ignore=
4 # E111: Indentation is not a multiple of four
5 E111,
6 # E114: Indentation is not a multiple of four (comment)
7 E114,
8 # E402: Module level import not at top of file
9 E402,
10 # E731: do not assign a lambda expression, use a def
11 E731,
12 # W503: Line break before binary operator
13 W503,
14 # W504: Line break after binary operator
15 W504
diff --git a/.github/workflows/test-ci.yml b/.github/workflows/test-ci.yml
new file mode 100644
index 00000000..19881858
--- /dev/null
+++ b/.github/workflows/test-ci.yml
@@ -0,0 +1,31 @@
1# GitHub actions workflow.
2# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions
3
4name: Test CI
5
6on:
7 push:
8 branches: [main, repo-1, stable, maint]
9 tags: [v*]
10
11jobs:
12 test:
13 strategy:
14 fail-fast: false
15 matrix:
16 os: [ubuntu-latest, macos-latest, windows-latest]
17 python-version: [3.6, 3.7, 3.8, 3.9]
18 runs-on: ${{ matrix.os }}
19
20 steps:
21 - uses: actions/checkout@v2
22 - name: Set up Python ${{ matrix.python-version }}
23 uses: actions/setup-python@v1
24 with:
25 python-version: ${{ matrix.python-version }}
26 - name: Install dependencies
27 run: |
28 python -m pip install --upgrade pip
29 pip install tox tox-gh-actions
30 - name: Test with tox
31 run: tox
diff --git a/.gitignore b/.gitignore
index 37962447..4e91be94 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
1*.asc
1*.egg-info/ 2*.egg-info/
2*.log 3*.log
3*.pyc 4*.pyc
@@ -6,6 +7,7 @@ __pycache__
6.repopickle_* 7.repopickle_*
7/repoc 8/repoc
8/.tox 9/.tox
10/.venv
9 11
10# PyCharm related 12# PyCharm related
11/.idea/ 13/.idea/
diff --git a/.mailmap b/.mailmap
index 905139d5..caf1c98f 100644
--- a/.mailmap
+++ b/.mailmap
@@ -4,6 +4,7 @@ Hu Xiuyun <xiuyun.hu@hisilicon.com> Hu xiuyun <xiuyun.hu@hisilicon.com
4Hu Xiuyun <xiuyun.hu@hisilicon.com> Hu Xiuyun <clouds08@qq.com> 4Hu Xiuyun <xiuyun.hu@hisilicon.com> Hu Xiuyun <clouds08@qq.com>
5Jelly Chen <chenguodong@huawei.com> chenguodong <chenguodong@huawei.com> 5Jelly Chen <chenguodong@huawei.com> chenguodong <chenguodong@huawei.com>
6Jia Bi <bijia@xiaomi.com> bijia <bijia@xiaomi.com> 6Jia Bi <bijia@xiaomi.com> bijia <bijia@xiaomi.com>
7Jiri Tyr <jiri.tyr@gmail.com> Jiri tyr <jiri.tyr@gmail.com>
7JoonCheol Park <jooncheol@gmail.com> Jooncheol Park <jooncheol@gmail.com> 8JoonCheol Park <jooncheol@gmail.com> Jooncheol Park <jooncheol@gmail.com>
8Sergii Pylypenko <x.pelya.x@gmail.com> pelya <x.pelya.x@gmail.com> 9Sergii Pylypenko <x.pelya.x@gmail.com> pelya <x.pelya.x@gmail.com>
9Shawn Pearce <sop@google.com> Shawn O. Pearce <sop@google.com> 10Shawn Pearce <sop@google.com> Shawn O. Pearce <sop@google.com>
diff --git a/README.md b/README.md
index 5c886359..5519e9aa 100644
--- a/README.md
+++ b/README.md
@@ -6,15 +6,29 @@ development workflow. Repo is not meant to replace Git, only to make it
6easier to work with Git. The repo command is an executable Python script 6easier to work with Git. The repo command is an executable Python script
7that you can put anywhere in your path. 7that you can put anywhere in your path.
8 8
9* Homepage: https://gerrit.googlesource.com/git-repo/ 9* Homepage: <https://gerrit.googlesource.com/git-repo/>
10* Bug reports: https://bugs.chromium.org/p/gerrit/issues/list?q=component:repo 10* Mailing list: [repo-discuss on Google Groups][repo-discuss]
11* Source: https://gerrit.googlesource.com/git-repo/ 11* Bug reports: <https://bugs.chromium.org/p/gerrit/issues/list?q=component:repo>
12* Overview: https://source.android.com/source/developing.html 12* Source: <https://gerrit.googlesource.com/git-repo/>
13* Docs: https://source.android.com/source/using-repo.html 13* Overview: <https://source.android.com/source/developing.html>
14* Docs: <https://source.android.com/source/using-repo.html>
14* [repo Manifest Format](./docs/manifest-format.md) 15* [repo Manifest Format](./docs/manifest-format.md)
15* [repo Hooks](./docs/repo-hooks.md) 16* [repo Hooks](./docs/repo-hooks.md)
16* [Submitting patches](./SUBMITTING_PATCHES.md) 17* [Submitting patches](./SUBMITTING_PATCHES.md)
17* Running Repo in [Microsoft Windows](./docs/windows.md) 18* Running Repo in [Microsoft Windows](./docs/windows.md)
19* GitHub mirror: <https://github.com/GerritCodeReview/git-repo>
20* Postsubmit tests: <https://github.com/GerritCodeReview/git-repo/actions>
21
22## Contact
23
24Please use the [repo-discuss] mailing list or [issue tracker] for questions.
25
26You can [file a new bug report][new-bug] under the "repo" component.
27
28Please do not e-mail individual developers for support.
29They do not have the bandwidth for it, and often times questions have already
30been asked on [repo-discuss] or bugs posted to the [issue tracker].
31So please search those sites first.
18 32
19## Install 33## Install
20 34
@@ -34,3 +48,8 @@ $ PATH="${HOME}/.bin:${PATH}"
34$ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/.bin/repo 48$ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/.bin/repo
35$ chmod a+rx ~/.bin/repo 49$ chmod a+rx ~/.bin/repo
36``` 50```
51
52
53[new-bug]: https://bugs.chromium.org/p/gerrit/issues/entry?template=Repo+tool+issue
54[issue tracker]: https://bugs.chromium.org/p/gerrit/issues/list?q=component:repo
55[repo-discuss]: https://groups.google.com/forum/#!forum/repo-discuss
diff --git a/SUBMITTING_PATCHES.md b/SUBMITTING_PATCHES.md
index 5021e7ee..0c189247 100644
--- a/SUBMITTING_PATCHES.md
+++ b/SUBMITTING_PATCHES.md
@@ -4,13 +4,13 @@
4 4
5 - Make small logical changes. 5 - Make small logical changes.
6 - Provide a meaningful commit message. 6 - Provide a meaningful commit message.
7 - Check for coding errors and style nits with pyflakes and flake8 7 - Check for coding errors and style nits with flake8.
8 - Make sure all code is under the Apache License, 2.0. 8 - Make sure all code is under the Apache License, 2.0.
9 - Publish your changes for review. 9 - Publish your changes for review.
10 - Make corrections if requested. 10 - Make corrections if requested.
11 - Verify your changes on gerrit so they can be submitted. 11 - Verify your changes on gerrit so they can be submitted.
12 12
13 `git push https://gerrit-review.googlesource.com/git-repo HEAD:refs/for/master` 13 `git push https://gerrit-review.googlesource.com/git-repo HEAD:refs/for/main`
14 14
15 15
16# Long Version 16# Long Version
@@ -38,34 +38,30 @@ If your description starts to get too long, that's a sign that you
38probably need to split up your commit to finer grained pieces. 38probably need to split up your commit to finer grained pieces.
39 39
40 40
41## Check for coding errors and style nits with pyflakes and flake8 41## Check for coding errors and style violations with flake8
42 42
43### Coding errors 43Run `flake8` on changed modules:
44 44
45Run `pyflakes` on changed modules: 45 flake8 file.py
46
47 pyflakes file.py
48 46
49Ideally there should be no new errors or warnings introduced. 47Note that repo generally follows [Google's Python Style Guide] rather than
48[PEP 8], with a couple of notable exceptions:
50 49
51### Style violations 50* Indentation is at 2 columns rather than 4
51* The maximum line length is 100 columns rather than 80
52 52
53Run `flake8` on changes modules: 53There should be no new errors or warnings introduced.
54 54
55 flake8 file.py 55Warnings that cannot be avoided without going against the Google Style Guide
56may be suppressed inline individally using a `# noqa` comment as described
57in the [flake8 documentation].
56 58
57Note that repo generally follows [Google's python style guide] rather than 59If there are many occurrences of the same warning, these may be suppressed for
58[PEP 8], so it's possible that the output of `flake8` will be quite noisy. 60the entire project in the included `.flake8` file.
59It's not mandatory to avoid all warnings, but at least the maximum line
60length should be followed.
61 61
62If there are many occurrences of the same warning that cannot be 62[Google's Python Style Guide]: https://google.github.io/styleguide/pyguide.html
63avoided without going against the Google style guide, these may be
64suppressed in the included `.flake8` file.
65
66[Google's python style guide]: https://google.github.io/styleguide/pyguide.html
67[PEP 8]: https://www.python.org/dev/peps/pep-0008/ 63[PEP 8]: https://www.python.org/dev/peps/pep-0008/
68 64[flake8 documentation]: https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html#in-line-ignoring-errors
69 65
70## Running tests 66## Running tests
71 67
@@ -154,7 +150,7 @@ Push your patches over HTTPS to the review server, possibly through
154a remembered remote to make this easier in the future: 150a remembered remote to make this easier in the future:
155 151
156 git config remote.review.url https://gerrit-review.googlesource.com/git-repo 152 git config remote.review.url https://gerrit-review.googlesource.com/git-repo
157 git config remote.review.push HEAD:refs/for/master 153 git config remote.review.push HEAD:refs/for/main
158 154
159 git push review 155 git push review
160 156
diff --git a/color.py b/color.py
index 5b3a282d..fdd72534 100644
--- a/color.py
+++ b/color.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -84,6 +82,7 @@ def _Color(fg=None, bg=None, attr=None):
84 code = '' 82 code = ''
85 return code 83 return code
86 84
85
87DEFAULT = None 86DEFAULT = None
88 87
89 88
diff --git a/command.py b/command.py
index 9e113f1a..b972a0be 100644
--- a/command.py
+++ b/command.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,25 +12,65 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
15import multiprocessing
17import os 16import os
18import optparse 17import optparse
19import platform
20import re 18import re
21import sys 19import sys
22 20
23from event_log import EventLog 21from event_log import EventLog
24from error import NoSuchProjectError 22from error import NoSuchProjectError
25from error import InvalidProjectGroupsError 23from error import InvalidProjectGroupsError
24import progress
25
26
27# Are we generating man-pages?
28GENERATE_MANPAGES = os.environ.get('_REPO_GENERATE_MANPAGES_') == ' indeed! '
29
30
31# Number of projects to submit to a single worker process at a time.
32# This number represents a tradeoff between the overhead of IPC and finer
33# grained opportunity for parallelism. This particular value was chosen by
34# iterating through powers of two until the overall performance no longer
35# improved. The performance of this batch size is not a function of the
36# number of cores on the system.
37WORKER_BATCH_SIZE = 32
38
39
40# How many jobs to run in parallel by default? This assumes the jobs are
41# largely I/O bound and do not hit the network.
42DEFAULT_LOCAL_JOBS = min(os.cpu_count(), 8)
26 43
27 44
28class Command(object): 45class Command(object):
29 """Base class for any command line action in repo. 46 """Base class for any command line action in repo.
30 """ 47 """
31 48
32 common = False 49 # Singleton for all commands to track overall repo command execution and
50 # provide event summary to callers. Only used by sync subcommand currently.
51 #
52 # NB: This is being replaced by git trace2 events. See git_trace2_event_log.
33 event_log = EventLog() 53 event_log = EventLog()
34 manifest = None 54
35 _optparse = None 55 # Whether this command is a "common" one, i.e. whether the user would commonly
56 # use it or it's a more uncommon command. This is used by the help command to
57 # show short-vs-full summaries.
58 COMMON = False
59
60 # Whether this command supports running in parallel. If greater than 0,
61 # it is the number of parallel jobs to default to.
62 PARALLEL_JOBS = None
63
64 def __init__(self, repodir=None, client=None, manifest=None, gitc_manifest=None,
65 git_event_log=None):
66 self.repodir = repodir
67 self.client = client
68 self.manifest = manifest
69 self.gitc_manifest = gitc_manifest
70 self.git_event_log = git_event_log
71
72 # Cache for the OptionParser property.
73 self._optparse = None
36 74
37 def WantPager(self, _opt): 75 def WantPager(self, _opt):
38 return False 76 return False
@@ -66,13 +104,39 @@ class Command(object):
66 usage = self.helpUsage.strip().replace('%prog', me) 104 usage = self.helpUsage.strip().replace('%prog', me)
67 except AttributeError: 105 except AttributeError:
68 usage = 'repo %s' % self.NAME 106 usage = 'repo %s' % self.NAME
69 self._optparse = optparse.OptionParser(usage=usage) 107 epilog = 'Run `repo help %s` to view the detailed manual.' % self.NAME
108 self._optparse = optparse.OptionParser(usage=usage, epilog=epilog)
109 self._CommonOptions(self._optparse)
70 self._Options(self._optparse) 110 self._Options(self._optparse)
71 return self._optparse 111 return self._optparse
72 112
73 def _Options(self, p): 113 def _CommonOptions(self, p, opt_v=True):
74 """Initialize the option parser. 114 """Initialize the option parser with common options.
115
116 These will show up for *all* subcommands, so use sparingly.
117 NB: Keep in sync with repo:InitParser().
75 """ 118 """
119 g = p.add_option_group('Logging options')
120 opts = ['-v'] if opt_v else []
121 g.add_option(*opts, '--verbose',
122 dest='output_mode', action='store_true',
123 help='show all output')
124 g.add_option('-q', '--quiet',
125 dest='output_mode', action='store_false',
126 help='only show errors')
127
128 if self.PARALLEL_JOBS is not None:
129 default = 'based on number of CPU cores'
130 if not GENERATE_MANPAGES:
131 # Only include active cpu count if we aren't generating man pages.
132 default = f'%default; {default}'
133 p.add_option(
134 '-j', '--jobs',
135 type=int, default=self.PARALLEL_JOBS,
136 help=f'number of jobs to run in parallel (default: {default})')
137
138 def _Options(self, p):
139 """Initialize the option parser with subcommand-specific options."""
76 140
77 def _RegisteredEnvironmentOptions(self): 141 def _RegisteredEnvironmentOptions(self):
78 """Get options that can be set from environment variables. 142 """Get options that can be set from environment variables.
@@ -98,6 +162,11 @@ class Command(object):
98 self.OptionParser.print_usage() 162 self.OptionParser.print_usage()
99 sys.exit(1) 163 sys.exit(1)
100 164
165 def CommonValidateOptions(self, opt, args):
166 """Validate common options."""
167 opt.quiet = opt.output_mode is False
168 opt.verbose = opt.output_mode is True
169
101 def ValidateOptions(self, opt, args): 170 def ValidateOptions(self, opt, args):
102 """Validate the user options & arguments before executing. 171 """Validate the user options & arguments before executing.
103 172
@@ -113,6 +182,44 @@ class Command(object):
113 """ 182 """
114 raise NotImplementedError 183 raise NotImplementedError
115 184
185 @staticmethod
186 def ExecuteInParallel(jobs, func, inputs, callback, output=None, ordered=False):
187 """Helper for managing parallel execution boiler plate.
188
189 For subcommands that can easily split their work up.
190
191 Args:
192 jobs: How many parallel processes to use.
193 func: The function to apply to each of the |inputs|. Usually a
194 functools.partial for wrapping additional arguments. It will be run
195 in a separate process, so it must be pickalable, so nested functions
196 won't work. Methods on the subcommand Command class should work.
197 inputs: The list of items to process. Must be a list.
198 callback: The function to pass the results to for processing. It will be
199 executed in the main thread and process the results of |func| as they
200 become available. Thus it may be a local nested function. Its return
201 value is passed back directly. It takes three arguments:
202 - The processing pool (or None with one job).
203 - The |output| argument.
204 - An iterator for the results.
205 output: An output manager. May be progress.Progess or color.Coloring.
206 ordered: Whether the jobs should be processed in order.
207
208 Returns:
209 The |callback| function's results are returned.
210 """
211 try:
212 # NB: Multiprocessing is heavy, so don't spin it up for one job.
213 if len(inputs) == 1 or jobs == 1:
214 return callback(None, output, (func(x) for x in inputs))
215 else:
216 with multiprocessing.Pool(jobs) as pool:
217 submit = pool.imap if ordered else pool.imap_unordered
218 return callback(pool, output, submit(func, inputs, chunksize=WORKER_BATCH_SIZE))
219 finally:
220 if isinstance(output, progress.Progress):
221 output.end()
222
116 def _ResetPathToProjectMap(self, projects): 223 def _ResetPathToProjectMap(self, projects):
117 self._by_path = dict((p.worktree, p) for p in projects) 224 self._by_path = dict((p.worktree, p) for p in projects)
118 225
@@ -123,9 +230,9 @@ class Command(object):
123 project = None 230 project = None
124 if os.path.exists(path): 231 if os.path.exists(path):
125 oldpath = None 232 oldpath = None
126 while path and \ 233 while (path and
127 path != oldpath and \ 234 path != oldpath and
128 path != manifest.topdir: 235 path != manifest.topdir):
129 try: 236 try:
130 project = self._by_path[path] 237 project = self._by_path[path]
131 break 238 break
@@ -156,9 +263,7 @@ class Command(object):
156 mp = manifest.manifestProject 263 mp = manifest.manifestProject
157 264
158 if not groups: 265 if not groups:
159 groups = mp.config.GetString('manifest.groups') 266 groups = manifest.GetGroupsStr()
160 if not groups:
161 groups = 'default,platform-' + platform.system().lower()
162 groups = [x for x in re.split(r'[,\s]+', groups) if x] 267 groups = [x for x in re.split(r'[,\s]+', groups) if x]
163 268
164 if not args: 269 if not args:
@@ -236,6 +341,7 @@ class InteractiveCommand(Command):
236 """Command which requires user interaction on the tty and 341 """Command which requires user interaction on the tty and
237 must not run within a pager, even if the user asks to. 342 must not run within a pager, even if the user asks to.
238 """ 343 """
344
239 def WantPager(self, _opt): 345 def WantPager(self, _opt):
240 return False 346 return False
241 347
@@ -244,6 +350,7 @@ class PagedCommand(Command):
244 """Command which defaults to output in a pager, as its 350 """Command which defaults to output in a pager, as its
245 display tends to be larger than one screen full. 351 display tends to be larger than one screen full.
246 """ 352 """
353
247 def WantPager(self, _opt): 354 def WantPager(self, _opt):
248 return True 355 return True
249 356
diff --git a/completion.bash b/completion.bash
new file mode 100644
index 00000000..09291d5c
--- /dev/null
+++ b/completion.bash
@@ -0,0 +1,156 @@
1# Copyright 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<tab>`, but not
18# `repo --time h<tab>`.
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 <command> 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 <branch> <projects>.
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 <projects>.
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 <subcommand> 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 <projects> *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 <branch> which we don't complete.
107 # CWORD=3+ are <projects> 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.
156complete -F __complete_repo -o bashdefault -o default repo
diff --git a/docs/internal-fs-layout.md b/docs/internal-fs-layout.md
new file mode 100644
index 00000000..af6a4523
--- /dev/null
+++ b/docs/internal-fs-layout.md
@@ -0,0 +1,263 @@
1# Repo internal filesystem layout
2
3A reference to the `.repo/` tree in repo client checkouts.
4Hopefully it's complete & up-to-date, but who knows!
5
6*** note
7**Warning**:
8This is meant for developers of the repo project itself as a quick reference.
9**Nothing** in here must be construed as ABI, or that repo itself will never
10change its internals in backwards incompatible ways.
11***
12
13[TOC]
14
15## .repo/ layout
16
17All content under `.repo/` is managed by `repo` itself with few exceptions.
18
19In general, you should not make manual changes in here.
20If a setting was initialized using an option to `repo init`, you should use that
21command to change the setting later on.
22It is always safe to re-run `repo init` in existing repo client checkouts.
23For example, if you want to change the manifest branch, you can simply run
24`repo init --manifest-branch=<new name>` and repo will take care of the rest.
25
26* `config`: Per-repo client checkout settings using [git-config] file format.
27* `.repo_config.json`: JSON cache of the `config` file for repo to
28 read/process quickly.
29
30### repo/ state
31
32* `repo/`: A git checkout of the repo project. This is how `repo` re-execs
33 itself to get the latest released version.
34
35 It tracks the git repository at `REPO_URL` using the `REPO_REV` branch.
36 Those are specified at `repo init` time using the `--repo-url=<REPO_URL>`
37 and `--repo-rev=<REPO_REV>` options.
38
39 Any changes made to this directory will usually be automatically discarded
40 by repo itself when it checks for updates. If you want to update to the
41 latest version of repo, use `repo selfupdate` instead. If you want to
42 change the git URL/branch that this tracks, re-run `repo init` with the new
43 settings.
44
45* `.repo_fetchtimes.json`: Used by `repo sync` to record stats when syncing
46 the various projects.
47
48### Manifests
49
50For more documentation on the manifest format, including the local_manifests
51support, see the [manifest-format.md] file.
52
53* `manifests/`: A git checkout of the manifest project. Its `.git/` state
54 points to the `manifest.git` bare checkout (see below). It tracks the git
55 branch specified at `repo init` time via `--manifest-branch`.
56
57 The local branch name is always `default` regardless of the remote tracking
58 branch. Do not get confused if the remote branch is not `default`, or if
59 there is a remote `default` that is completely different!
60
61 No manual changes should be made in here as it will just confuse repo and
62 it won't automatically recover causing no new changes to be picked up.
63
64* `manifests.git/`: A bare checkout of the manifest project. It tracks the
65 git repository specified at `repo init` time via `--manifest-url`.
66
67 No manual changes should be made in here as it will just confuse repo.
68 If you want to switch the tracking settings, re-run `repo init` with the
69 new settings.
70
71* `manifest.xml`: The manifest that repo uses. It is generated at `repo init`
72 and uses the `--manifest-name` to determine what manifest file to load next
73 out of `manifests/`.
74
75 Do not try to modify this to load other manifests as it will confuse repo.
76 If you want to switch manifest files, re-run `repo init` with the new
77 setting.
78
79 Older versions of repo managed this with symlinks.
80
81* `manifest.xml -> manifests/<manifest-name>.xml`: A symlink to the manifest
82 that the user wishes to sync. It is specified at `repo init` time via
83 `--manifest-name`.
84
85
86* `manifests.git/.repo_config.json`: JSON cache of the `manifests.git/config`
87 file for repo to read/process quickly.
88
89* `local_manifest.xml` (*Deprecated*): User-authored tweaks to the manifest
90 used to sync. See [local manifests] for more details.
91* `local_manifests/`: Directory of user-authored manifest fragments to tweak
92 the manifest used to sync. See [local manifests] for more details.
93
94### Project objects
95
96*** note
97**Warning**: Please do not use repo's approach to projects/ & project-objects/
98layouts as a model for other tools to implement similar approaches.
99It has a number of known downsides like:
100* [Symlinks do not work well under Windows](./windows.md).
101* Git sometimes replaces symlinks under .git/ with real files (under unknown
102 circumstances), and then the internal state gets out of sync, and data loss
103 may ensue.
104* When sharing project-objects between multiple project checkouts, Git might
105 automatically run `gc` or `prune` which may lead to data loss or corruption
106 (since those operate on leaf projects and miss refs in other leaves). See
107 https://gerrit-review.googlesource.com/c/git-repo/+/254392 for more details.
108
109Instead, you should use standard Git workflows like [git worktree] or
110[gitsubmodules] with [superprojects].
111***
112
113* `copy-link-files.json`: Tracking file used by `repo sync` to determine when
114 copyfile or linkfile are added or removed and need corresponding updates.
115* `project.list`: Tracking file used by `repo sync` to determine when projects
116 are added or removed and need corresponding updates in the checkout.
117* `projects/`: Bare checkouts of every project synced by the manifest. The
118 filesystem layout matches the `<project path=...` setting in the manifest
119 (i.e. where it's checked out in the repo client source tree). Those
120 checkouts will symlink their `.git/` state to paths under here.
121
122 Some git state is further split out under `project-objects/`.
123* `project-objects/`: Git objects that are safe to share across multiple
124 git checkouts. The filesystem layout matches the `<project name=...`
125 setting in the manifest (i.e. the path on the remote server) with a `.git`
126 suffix. This allows for multiple checkouts of the same remote git repo to
127 share their objects. For example, you could have different branches of
128 `foo/bar.git` checked out to `foo/bar-main`, `foo/bar-release`, etc...
129 There will be multiple trees under `projects/` for each one, but only one
130 under `project-objects/`.
131
132 This layout is designed to allow people to sync against different remotes
133 (e.g. a local mirror & a public review server) while avoiding duplicating
134 the content. However, this can run into problems if different remotes use
135 the same path on their respective servers. Best to avoid that.
136* `subprojects/`: Like `projects/`, but for git submodules.
137* `subproject-objects/`: Like `project-objects/`, but for git submodules.
138* `worktrees/`: Bare checkouts of every project synced by the manifest. The
139 filesystem layout matches the `<project name=...` setting in the manifest
140 (i.e. the path on the remote server) with a `.git` suffix. This has the
141 same advantages as the `project-objects/` layout above.
142
143 This is used when [git worktree]'s are enabled.
144
145### Global settings
146
147The `.repo/manifests.git/config` file is used to track settings for the entire
148repo client checkout.
149
150Most settings use the `[repo]` section to avoid conflicts with git.
151
152Everything under `[repo.syncstate.*]` is used to keep track of sync details for logging
153purposes.
154
155User controlled settings are initialized when running `repo init`.
156
157| Setting | `repo init` Option | Use/Meaning |
158|------------------- |---------------------------|-------------|
159| manifest.groups | `--groups` & `--platform` | The manifest groups to sync |
160| manifest.standalone | `--standalone-manifest` | Download manifest as static file instead of creating checkout |
161| repo.archive | `--archive` | Use `git archive` for checkouts |
162| repo.clonebundle | `--clone-bundle` | Whether the initial sync used clone.bundle explicitly |
163| repo.clonefilter | `--clone-filter` | Filter setting when using [partial git clones] |
164| repo.depth | `--depth` | Create shallow checkouts when cloning |
165| repo.dissociate | `--dissociate` | Dissociate from any reference/mirrors after initial clone |
166| repo.mirror | `--mirror` | Checkout is a repo mirror |
167| repo.partialclone | `--partial-clone` | Create [partial git clones] |
168| repo.partialcloneexclude | `--partial-clone-exclude` | Comma-delimited list of project names (not paths) to exclude while using [partial git clones] |
169| repo.reference | `--reference` | Reference repo client checkout |
170| repo.submodules | `--submodules` | Sync git submodules |
171| repo.superproject | `--use-superproject` | Sync [superproject] |
172| repo.worktree | `--worktree` | Use [git worktree] for checkouts |
173| user.email | `--config-name` | User's e-mail address; Copied into `.git/config` when checking out a new project |
174| user.name | `--config-name` | User's name; Copied into `.git/config` when checking out a new project |
175
176[partial git clones]: https://git-scm.com/docs/gitrepository-layout#_code_partialclone_code
177[superproject]: https://en.wikibooks.org/wiki/Git/Submodules_and_Superprojects
178
179### Repo hooks settings
180
181For more details on this feature, see the [repo-hooks docs](./repo-hooks.md).
182We'll just discuss the internal configuration settings.
183These are stored in the registered `<repo-hooks>` project itself, so if the
184manifest switches to a different project, the settings will not be copied.
185
186| Setting | Use/Meaning |
187|--------------------------------------|-------------|
188| repo.hooks.\<hook\>.approvedmanifest | User approval for secure manifest sources (e.g. https://) |
189| repo.hooks.\<hook\>.approvedhash | User approval for insecure manifest sources (e.g. http://) |
190
191
192For example, if our manifest had the following entries, we would store settings
193under `.repo/projects/src/repohooks.git/config` (which would be reachable via
194`git --git-dir=src/repohooks/.git config`).
195```xml
196 <project path="src/repohooks" name="chromiumos/repohooks" ... />
197 <repo-hooks in-project="chromiumos/repohooks" ... />
198```
199
200If `<hook>` is `pre-upload`, the `.git/config` setting might be:
201```ini
202[repo "hooks.pre-upload"]
203 approvedmanifest = https://chromium.googlesource.com/chromiumos/manifest
204```
205
206## Per-project settings
207
208These settings are somewhat meant to be tweaked by the user on a per-project
209basis (e.g. `git config` in a checked out source repo).
210
211Where possible, we re-use standard git settings to avoid confusion, and we
212refrain from documenting those, so see [git-config] documentation instead.
213
214See `repo help upload` for documentation on `[review]` settings.
215
216The `[remote]` settings are automatically populated/updated from the manifest.
217
218The `[branch]` settings are updated by `repo start` and `git branch`.
219
220| Setting | Subcommands | Use/Meaning |
221|-------------------------------|---------------|-------------|
222| review.\<url\>.autocopy | upload | Automatically add to `--cc=<value>` |
223| review.\<url\>.autoreviewer | upload | Automatically add to `--reviewers=<value>` |
224| review.\<url\>.autoupload | upload | Automatically answer "yes" or "no" to all prompts |
225| review.\<url\>.uploadhashtags | upload | Automatically add to `--hashtag=<value>` |
226| review.\<url\>.uploadlabels | upload | Automatically add to `--label=<value>` |
227| review.\<url\>.uploadnotify | upload | [Notify setting][upload-notify] to use |
228| review.\<url\>.uploadtopic | upload | Default [topic] to use |
229| review.\<url\>.username | upload | Override username with `ssh://` review URIs |
230| remote.\<remote\>.fetch | sync | Set of refs to fetch |
231| remote.\<remote\>.projectname | \<network\> | The name of the project as it exists in Gerrit review |
232| remote.\<remote\>.pushurl | upload | The base URI for pushing CLs |
233| remote.\<remote\>.review | upload | The URI of the Gerrit review server |
234| remote.\<remote\>.url | sync & upload | The URI of the git project to fetch |
235| branch.\<branch\>.merge | sync & upload | The branch to merge & upload & track |
236| branch.\<branch\>.remote | sync & upload | The remote to track |
237
238## ~/ dotconfig layout
239
240Repo will create & maintain a few files in the user's home directory.
241
242* `.repoconfig/`: Repo's per-user directory for all random config files/state.
243* `.repoconfig/config`: Per-user settings using [git-config] file format.
244* `.repoconfig/keyring-version`: Cache file for checking if the gnupg subdir
245 has all the same keys as the repo launcher. Used to avoid running gpg
246 constantly as that can be quite slow.
247* `.repoconfig/gnupg/`: GnuPG's internal state directory used when repo needs
248 to run `gpg`. This provides isolation from the user's normal `~/.gnupg/`.
249
250* `.repoconfig/.repo_config.json`: JSON cache of the `.repoconfig/config`
251 file for repo to read/process quickly.
252* `.repo_.gitconfig.json`: JSON cache of the `.gitconfig` file for repo to
253 read/process quickly.
254
255
256[git-config]: https://git-scm.com/docs/git-config
257[git worktree]: https://git-scm.com/docs/git-worktree
258[gitsubmodules]: https://git-scm.com/docs/gitsubmodules
259[manifest-format.md]: ./manifest-format.md
260[local manifests]: ./manifest-format.md#Local-Manifests
261[superprojects]: https://en.wikibooks.org/wiki/Git/Submodules_and_Superprojects
262[topic]: https://gerrit-review.googlesource.com/Documentation/intro-user.html#topics
263[upload-notify]: https://gerrit-review.googlesource.com/Documentation/user-upload.html#notify
diff --git a/docs/manifest-format.md b/docs/manifest-format.md
index 93d9b960..8e0049b3 100644
--- a/docs/manifest-format.md
+++ b/docs/manifest-format.md
@@ -21,6 +21,7 @@ following DTD:
21 21
22```xml 22```xml
23<!DOCTYPE manifest [ 23<!DOCTYPE manifest [
24
24 <!ELEMENT manifest (notice?, 25 <!ELEMENT manifest (notice?,
25 remote*, 26 remote*,
26 default?, 27 default?,
@@ -29,11 +30,13 @@ following DTD:
29 project*, 30 project*,
30 extend-project*, 31 extend-project*,
31 repo-hooks?, 32 repo-hooks?,
33 superproject?,
34 contactinfo?,
32 include*)> 35 include*)>
33 36
34 <!ELEMENT notice (#PCDATA)> 37 <!ELEMENT notice (#PCDATA)>
35 38
36 <!ELEMENT remote EMPTY> 39 <!ELEMENT remote (annotation*)>
37 <!ATTLIST remote name ID #REQUIRED> 40 <!ATTLIST remote name ID #REQUIRED>
38 <!ATTLIST remote alias CDATA #IMPLIED> 41 <!ATTLIST remote alias CDATA #IMPLIED>
39 <!ATTLIST remote fetch CDATA #REQUIRED> 42 <!ATTLIST remote fetch CDATA #REQUIRED>
@@ -87,21 +90,39 @@ following DTD:
87 <!ELEMENT extend-project EMPTY> 90 <!ELEMENT extend-project EMPTY>
88 <!ATTLIST extend-project name CDATA #REQUIRED> 91 <!ATTLIST extend-project name CDATA #REQUIRED>
89 <!ATTLIST extend-project path CDATA #IMPLIED> 92 <!ATTLIST extend-project path CDATA #IMPLIED>
93 <!ATTLIST extend-project dest-path CDATA #IMPLIED>
90 <!ATTLIST extend-project groups CDATA #IMPLIED> 94 <!ATTLIST extend-project groups CDATA #IMPLIED>
91 <!ATTLIST extend-project revision CDATA #IMPLIED> 95 <!ATTLIST extend-project revision CDATA #IMPLIED>
96 <!ATTLIST extend-project remote CDATA #IMPLIED>
92 97
93 <!ELEMENT remove-project EMPTY> 98 <!ELEMENT remove-project EMPTY>
94 <!ATTLIST remove-project name CDATA #REQUIRED> 99 <!ATTLIST remove-project name CDATA #REQUIRED>
100 <!ATTLIST remove-project optional CDATA #IMPLIED>
95 101
96 <!ELEMENT repo-hooks EMPTY> 102 <!ELEMENT repo-hooks EMPTY>
97 <!ATTLIST repo-hooks in-project CDATA #REQUIRED> 103 <!ATTLIST repo-hooks in-project CDATA #REQUIRED>
98 <!ATTLIST repo-hooks enabled-list CDATA #REQUIRED> 104 <!ATTLIST repo-hooks enabled-list CDATA #REQUIRED>
99 105
106 <!ELEMENT superproject EMPTY>
107 <!ATTLIST superproject name CDATA #REQUIRED>
108 <!ATTLIST superproject remote IDREF #IMPLIED>
109 <!ATTLIST superproject revision CDATA #IMPLIED>
110
111 <!ELEMENT contactinfo EMPTY>
112 <!ATTLIST contactinfo bugurl CDATA #REQUIRED>
113
100 <!ELEMENT include EMPTY> 114 <!ELEMENT include EMPTY>
101 <!ATTLIST include name CDATA #REQUIRED> 115 <!ATTLIST include name CDATA #REQUIRED>
116 <!ATTLIST include groups CDATA #IMPLIED>
102]> 117]>
103``` 118```
104 119
120For compatibility purposes across repo releases, all unknown elements are
121silently ignored. However, repo reserves all possible names for itself for
122future use. If you want to use custom elements, the `x-*` namespace is
123reserved for that purpose, and repo guarantees to never allocate any
124corresponding names.
125
105A description of the elements and their attributes follows. 126A description of the elements and their attributes follows.
106 127
107 128
@@ -109,6 +130,10 @@ A description of the elements and their attributes follows.
109 130
110The root element of the file. 131The root element of the file.
111 132
133### Element notice
134
135Arbitrary text that is displayed to users whenever `repo sync` finishes.
136The content is simply passed through as it exists in the manifest.
112 137
113### Element remote 138### Element remote
114 139
@@ -141,8 +166,8 @@ Attribute `review`: Hostname of the Gerrit server where reviews
141are uploaded to by `repo upload`. This attribute is optional; 166are uploaded to by `repo upload`. This attribute is optional;
142if not specified then `repo upload` will not function. 167if not specified then `repo upload` will not function.
143 168
144Attribute `revision`: Name of a Git branch (e.g. `master` or 169Attribute `revision`: Name of a Git branch (e.g. `main` or
145`refs/heads/master`). Remotes with their own revision will override 170`refs/heads/main`). Remotes with their own revision will override
146the default revision. 171the default revision.
147 172
148### Element default 173### Element default
@@ -155,11 +180,11 @@ Attribute `remote`: Name of a previously defined remote element.
155Project elements lacking a remote attribute of their own will use 180Project elements lacking a remote attribute of their own will use
156this remote. 181this remote.
157 182
158Attribute `revision`: Name of a Git branch (e.g. `master` or 183Attribute `revision`: Name of a Git branch (e.g. `main` or
159`refs/heads/master`). Project elements lacking their own 184`refs/heads/main`). Project elements lacking their own
160revision attribute will use this revision. 185revision attribute will use this revision.
161 186
162Attribute `dest-branch`: Name of a Git branch (e.g. `master`). 187Attribute `dest-branch`: Name of a Git branch (e.g. `main`).
163Project elements not setting their own `dest-branch` will inherit 188Project elements not setting their own `dest-branch` will inherit
164this value. If this value is not set, projects will use `revision` 189this value. If this value is not set, projects will use `revision`
165by default instead. 190by default instead.
@@ -235,24 +260,37 @@ name will be prefixed by the parent's.
235The project name must match the name Gerrit knows, if Gerrit is 260The project name must match the name Gerrit knows, if Gerrit is
236being used for code reviews. 261being used for code reviews.
237 262
263"name" must not be empty, and may not be an absolute path or use "." or ".."
264path components. It is always interpreted relative to the remote's fetch
265settings, so if a different base path is needed, declare a different remote
266with the new settings needed.
267These restrictions are not enforced for [Local Manifests].
268
238Attribute `path`: An optional path relative to the top directory 269Attribute `path`: An optional path relative to the top directory
239of the repo client where the Git working directory for this project 270of the repo client where the Git working directory for this project
240should be placed. If not supplied the project name is used. 271should be placed. If not supplied the project "name" is used.
241If the project has a parent element, its path will be prefixed 272If the project has a parent element, its path will be prefixed
242by the parent's. 273by the parent's.
243 274
275"path" may not be an absolute path or use "." or ".." path components.
276These restrictions are not enforced for [Local Manifests].
277
278If you want to place files into the root of the checkout (e.g. a README or
279Makefile or another build script), use the [copyfile] or [linkfile] elements
280instead.
281
244Attribute `remote`: Name of a previously defined remote element. 282Attribute `remote`: Name of a previously defined remote element.
245If not supplied the remote given by the default element is used. 283If not supplied the remote given by the default element is used.
246 284
247Attribute `revision`: Name of the Git branch the manifest wants 285Attribute `revision`: Name of the Git branch the manifest wants
248to track for this project. Names can be relative to refs/heads 286to track for this project. Names can be relative to refs/heads
249(e.g. just "master") or absolute (e.g. "refs/heads/master"). 287(e.g. just "main") or absolute (e.g. "refs/heads/main").
250Tags and/or explicit SHA-1s should work in theory, but have not 288Tags and/or explicit SHA-1s should work in theory, but have not
251been extensively tested. If not supplied the revision given by 289been extensively tested. If not supplied the revision given by
252the remote element is used if applicable, else the default 290the remote element is used if applicable, else the default
253element is used. 291element is used.
254 292
255Attribute `dest-branch`: Name of a Git branch (e.g. `master`). 293Attribute `dest-branch`: Name of a Git branch (e.g. `main`).
256When using `repo upload`, changes will be submitted for code 294When using `repo upload`, changes will be submitted for code
257review on this branch. If unspecified both here and in the 295review on this branch. If unspecified both here and in the
258default element, `revision` is used instead. 296default element, `revision` is used instead.
@@ -261,7 +299,7 @@ Attribute `groups`: List of groups to which this project belongs,
261whitespace or comma separated. All projects belong to the group 299whitespace or comma separated. All projects belong to the group
262"all", and each project automatically belongs to a group of 300"all", and each project automatically belongs to a group of
263its name:`name` and path:`path`. E.g. for 301its name:`name` and path:`path`. E.g. for
264<project name="monkeys" path="barrel-of"/>, that project 302`<project name="monkeys" path="barrel-of"/>`, that project
265definition is implicitly in the following manifest groups: 303definition is implicitly in the following manifest groups:
266default, name:monkeys, and path:barrel-of. If you place a project in the 304default, name:monkeys, and path:barrel-of. If you place a project in the
267group "notdefault", it will not be automatically downloaded by repo. 305group "notdefault", it will not be automatically downloaded by repo.
@@ -300,21 +338,29 @@ against changes to the original manifest.
300Attribute `path`: If specified, limit the change to projects checked out 338Attribute `path`: If specified, limit the change to projects checked out
301at the specified path, rather than all projects with the given name. 339at the specified path, rather than all projects with the given name.
302 340
341Attribute `dest-path`: If specified, a path relative to the top directory
342of the repo client where the Git working directory for this project
343should be placed. This is used to move a project in the checkout by
344overriding the existing `path` setting.
345
303Attribute `groups`: List of additional groups to which this project 346Attribute `groups`: List of additional groups to which this project
304belongs. Same syntax as the corresponding element of `project`. 347belongs. Same syntax as the corresponding element of `project`.
305 348
306Attribute `revision`: If specified, overrides the revision of the original 349Attribute `revision`: If specified, overrides the revision of the original
307project. Same syntax as the corresponding element of `project`. 350project. Same syntax as the corresponding element of `project`.
308 351
352Attribute `remote`: If specified, overrides the remote of the original
353project. Same syntax as the corresponding element of `project`.
354
309### Element annotation 355### Element annotation
310 356
311Zero or more annotation elements may be specified as children of a 357Zero or more annotation elements may be specified as children of a
312project element. Each element describes a name-value pair that will be 358project or remote element. Each element describes a name-value pair.
313exported into each project's environment during a 'forall' command, 359For projects, this name-value pair will be exported into each project's
314prefixed with REPO__. In addition, there is an optional attribute 360environment during a 'forall' command, prefixed with `REPO__`. In addition,
315"keep" which accepts the case insensitive values "true" (default) or 361there is an optional attribute "keep" which accepts the case insensitive values
316"false". This attribute determines whether or not the annotation will 362"true" (default) or "false". This attribute determines whether or not the
317be kept when exported with the manifest subcommand. 363annotation will be kept when exported with the manifest subcommand.
318 364
319### Element copyfile 365### Element copyfile
320 366
@@ -338,7 +384,7 @@ It's just like copyfile and runs at the same time as copyfile but
338instead of copying it creates a symlink. 384instead of copying it creates a symlink.
339 385
340The symlink is created at "dest" (relative to the top of the tree) and 386The symlink is created at "dest" (relative to the top of the tree) and
341points to the path specified by "src". 387points to the path specified by "src" which is a path in the project.
342 388
343Parent directories of "dest" will be automatically created if missing. 389Parent directories of "dest" will be automatically created if missing.
344 390
@@ -355,6 +401,62 @@ This element is mostly useful in a local manifest file, where
355the user can remove a project, and possibly replace it with their 401the user can remove a project, and possibly replace it with their
356own definition. 402own definition.
357 403
404Attribute `optional`: Set to true to ignore remove-project elements with no
405matching `project` element.
406
407### Element repo-hooks
408
409NB: See the [practical documentation](./repo-hooks.md) for using repo hooks.
410
411Only one repo-hooks element may be specified at a time.
412Attempting to redefine it will fail to parse.
413
414Attribute `in-project`: The project where the hooks are defined. The value
415must match the `name` attribute (**not** the `path` attribute) of a previously
416defined `project` element.
417
418Attribute `enabled-list`: List of hooks to use, whitespace or comma separated.
419
420### Element superproject
421
422***
423*Note*: This is currently a WIP.
424***
425
426NB: See the [git superprojects documentation](
427https://en.wikibooks.org/wiki/Git/Submodules_and_Superprojects) for background
428information.
429
430This element is used to specify the URL of the superproject. It has "name" and
431"remote" as atrributes. Only "name" is required while the others have
432reasonable defaults. At most one superproject may be specified.
433Attempting to redefine it will fail to parse.
434
435Attribute `name`: A unique name for the superproject. This attribute has the
436same meaning as project's name attribute. See the
437[element project](#element-project) for more information.
438
439Attribute `remote`: Name of a previously defined remote element.
440If not supplied the remote given by the default element is used.
441
442Attribute `revision`: Name of the Git branch the manifest wants
443to track for this superproject. If not supplied the revision given
444by the remote element is used if applicable, else the default
445element is used.
446
447### Element contactinfo
448
449***
450*Note*: This is currently a WIP.
451***
452
453This element is used to let manifest authors self-register contact info.
454It has "bugurl" as a required atrribute. This element can be repeated,
455and any later entries will clobber earlier ones. This would allow manifest
456authors who extend manifests to specify their own contact info.
457
458Attribute `bugurl`: The URL to file a bug against the manifest owner.
459
358### Element include 460### Element include
359 461
360This element provides the capability of including another manifest 462This element provides the capability of including another manifest
@@ -364,8 +466,15 @@ target manifest to include - it must be a usable manifest on its own.
364Attribute `name`: the manifest to include, specified relative to 466Attribute `name`: the manifest to include, specified relative to
365the manifest repository's root. 467the manifest repository's root.
366 468
469"name" may not be an absolute path or use "." or ".." path components.
470These restrictions are not enforced for [Local Manifests].
471
472Attribute `groups`: List of additional groups to which all projects
473in the included manifest belong. This appends and recurses, meaning
474all projects in sub-manifests carry all parent include groups.
475Same syntax as the corresponding element of `project`.
367 476
368## Local Manifests 477## Local Manifests {#local-manifests}
369 478
370Additional remotes and projects may be added through local manifest 479Additional remotes and projects may be added through local manifest
371files stored in `$TOP_DIR/.repo/local_manifests/*.xml`. 480files stored in `$TOP_DIR/.repo/local_manifests/*.xml`.
@@ -392,10 +501,12 @@ these extra projects.
392Manifest files stored in `$TOP_DIR/.repo/local_manifests/*.xml` will 501Manifest files stored in `$TOP_DIR/.repo/local_manifests/*.xml` will
393be loaded in alphabetical order. 502be loaded in alphabetical order.
394 503
395Additional remotes and projects may also be added through a local 504Projects from local manifest files are added into
396manifest, stored in `$TOP_DIR/.repo/local_manifest.xml`. This method 505local::<local manifest filename> group.
397is deprecated in favor of using multiple manifest files as mentioned 506
398above. 507The legacy `$TOP_DIR/.repo/local_manifest.xml` path is no longer supported.
508
399 509
400If `$TOP_DIR/.repo/local_manifest.xml` exists, it will be loaded before 510[copyfile]: #Element-copyfile
401any manifest files stored in `$TOP_DIR/.repo/local_manifests/*.xml`. 511[linkfile]: #Element-linkfile
512[Local Manifests]: #local-manifests
diff --git a/docs/python-support.md b/docs/python-support.md
index a5c490a8..3eaaba33 100644
--- a/docs/python-support.md
+++ b/docs/python-support.md
@@ -18,13 +18,13 @@ Bugfixes may be added on a best-effort basis or from the community, but largely
18no new features will be added, nor is support guaranteed. 18no new features will be added, nor is support guaranteed.
19 19
20Users can select this during `repo init` time via the [repo launcher]. 20Users can select this during `repo init` time via the [repo launcher].
21Otherwise the default branches (e.g. stable & master) will be used which will 21Otherwise the default branches (e.g. stable & main) will be used which will
22require Python 3. 22require Python 3.
23 23
24This means the [repo launcher] needs to support both Python 2 & Python 3, but 24This means the [repo launcher] needs to support both Python 2 & Python 3, but
25since it doesn't import any other repo code, this shouldn't be too problematic. 25since it doesn't import any other repo code, this shouldn't be too problematic.
26 26
27The master branch will require Python 3.6 at a minimum. 27The main branch will require Python 3.6 at a minimum.
28If the system has an older version of Python 3, then users will have to select 28If the system has an older version of Python 3, then users will have to select
29the legacy Python 2 branch instead. 29the legacy Python 2 branch instead.
30 30
diff --git a/docs/release-process.md b/docs/release-process.md
index 22c2fd19..f71a4110 100644
--- a/docs/release-process.md
+++ b/docs/release-process.md
@@ -5,6 +5,37 @@ related topics and flows.
5 5
6[TOC] 6[TOC]
7 7
8## Schedule
9
10There is no specific schedule for when releases are made.
11Usually it's more along the lines of "enough minor changes have been merged",
12or "there's a known issue the maintainers know should get fixed".
13If you find a fix has been merged for an issue important to you, but hasn't been
14released after a week or so, feel free to [contact] us to request a new release.
15
16### Release Freezes {#freeze}
17
18We try to observe a regular schedule for when **not** to release.
19If something goes wrong, staff need to be active in order to respond quickly &
20effectively.
21We also don't want to disrupt non-Google organizations if possible.
22
23We generally follow the rules:
24
25* Release during Mon - Thu, 9:00 - 14:00 [US PT]
26* Avoid holidays
27 * All regular [US holidays]
28 * Large international ones if possible
29 * All the various [New Years]
30 * Jan 1 in Gregorian calendar is the most obvious
31 * Check for large Lunar New Years too
32* Follow the normal [Google production freeze schedule]
33
34[US holidays]: https://en.wikipedia.org/wiki/Federal_holidays_in_the_United_States
35[US PT]: https://en.wikipedia.org/wiki/Pacific_Time_Zone
36[New Years]: https://en.wikipedia.org/wiki/New_Year
37[Google production freeze schedule]: http://goto.google.com/prod-freeze
38
8## Launcher script 39## Launcher script
9 40
10The main repo script serves as a standalone program and is often referred to as 41The main repo script serves as a standalone program and is often referred to as
@@ -49,11 +80,12 @@ control how repo finds updates:
49 80
50* `--repo-url`: This tells repo where to clone the full repo project itself. 81* `--repo-url`: This tells repo where to clone the full repo project itself.
51 It defaults to the official project (`REPO_URL` in the launcher script). 82 It defaults to the official project (`REPO_URL` in the launcher script).
52* `--repo-branch`: This tells repo which branch to use for the full project. 83* `--repo-rev`: This tells repo which branch to use for the full project.
53 It defaults to the `stable` branch (`REPO_REV` in the launcher script). 84 It defaults to the `stable` branch (`REPO_REV` in the launcher script).
54 85
55Whenever `repo sync` is run, repo will check to see if an update is available. 86Whenever `repo sync` is run, repo will, once every 24 hours, see if an update
56It fetches the latest repo-branch from the repo-url. 87is available.
88It fetches the latest repo-rev from the repo-url.
57Then it verifies that the latest commit in the branch has a valid signed tag 89Then it verifies that the latest commit in the branch has a valid signed tag
58using `git tag -v` (which uses gpg). 90using `git tag -v` (which uses gpg).
59If the tag is valid, then repo will update its internal checkout to it. 91If the tag is valid, then repo will update its internal checkout to it.
@@ -64,9 +96,14 @@ If that tag is valid, then repo will warn and use that commit instead.
64 96
65If that tag cannot be verified, it gives up and forces the user to resolve. 97If that tag cannot be verified, it gives up and forces the user to resolve.
66 98
99### Force an update
100
101The `repo selfupdate` command can be used to force an immediate update.
102It is not subject to the 24 hour limitation.
103
67## Branch management 104## Branch management
68 105
69All development happens on the `master` branch and should generally be stable. 106All development happens on the `main` branch and should generally be stable.
70 107
71Since the repo launcher defaults to tracking the `stable` branch, it is not 108Since the repo launcher defaults to tracking the `stable` branch, it is not
72normally updated until a new release is available. 109normally updated until a new release is available.
@@ -81,7 +118,7 @@ For example, when `stable` moves from `v1.10.x` to `v1.11.x`, then the `maint`
81branch will be updated from `v1.9.x` to `v1.10.x`. 118branch will be updated from `v1.9.x` to `v1.10.x`.
82 119
83We don't have parallel release branches/series. 120We don't have parallel release branches/series.
84Typically all tags are made against the `master` branch and then pushed to the 121Typically all tags are made against the `main` branch and then pushed to the
85`stable` branch to make it available to the rest of the world. 122`stable` branch to make it available to the rest of the world.
86Since repo doesn't typically see a lot of changes, this tends to be OK. 123Since repo doesn't typically see a lot of changes, this tends to be OK.
87 124
@@ -89,10 +126,10 @@ Since repo doesn't typically see a lot of changes, this tends to be OK.
89 126
90When you want to create a new release, you'll need to select a good version and 127When you want to create a new release, you'll need to select a good version and
91create a signed tag using a key registered in repo itself. 128create a signed tag using a key registered in repo itself.
92Typically we just tag the latest version of the `master` branch. 129Typically we just tag the latest version of the `main` branch.
93The tag could be pushed now, but it won't be used by clients normally (since the 130The tag could be pushed now, but it won't be used by clients normally (since the
94default `repo-branch` setting is `stable`). 131default `repo-rev` setting is `stable`).
95This would allow some early testing on systems who explicitly select `master`. 132This would allow some early testing on systems who explicitly select `main`.
96 133
97### Creating a signed tag 134### Creating a signed tag
98 135
@@ -113,7 +150,7 @@ $ export GNUPGHOME=~/.gnupg/repo/
113$ gpg -K 150$ gpg -K
114 151
115# Pick whatever branch or commit you want to tag. 152# Pick whatever branch or commit you want to tag.
116$ r=master 153$ r=main
117 154
118# Pick the new version. 155# Pick the new version.
119$ t=1.12.10 156$ t=1.12.10
@@ -161,7 +198,144 @@ You can create a short changelog using the command:
161$ git log --format="%h (%aN) %s" --no-merges origin/stable..$r 198$ git log --format="%h (%aN) %s" --no-merges origin/stable..$r
162``` 199```
163 200
164 201## Project References
202
203Here's a table showing the relationship of major tools, their EOL dates, and
204their status in Ubuntu & Debian.
205Those distros tend to be good indicators of how long we need to support things.
206
207Things in bold indicate stuff to take note of, but does not guarantee that we
208still support them.
209Things in italics are things we used to care about but probably don't anymore.
210
211| Date | EOL | [Git][rel-g] | [Python][rel-p] | [SSH][rel-o] | [Ubuntu][rel-u] / [Debian][rel-d] | Git | Python | SSH |
212|:--------:|:------------:|:------------:|:---------------:|:------------:|-----------------------------------|-----|--------|-----|
213| Apr 2008 | | | | 5.0 |
214| Jun 2008 | | | | 5.1 |
215| Oct 2008 | *Oct 2013* | | 2.6.0 | | *10.04 Lucid* - 10.10 Maverick / *Squeeze* |
216| Dec 2008 | *Feb 2009* | | 3.0.0 |
217| Feb 2009 | | | | 5.2 |
218| Feb 2009 | *Mar 2012* | | | | Debian 5 Lenny | 1.5.6.5 | 2.5.2 |
219| Jun 2009 | *Jun 2016* | | 3.1.0 | | *10.04 Lucid* - 10.10 Maverick / *Squeeze* |
220| Sep 2009 | | | | 5.3 | *10.04 Lucid* |
221| Feb 2010 | *Oct 2012* | 1.7.0 | | | *10.04 Lucid* - *12.04 Precise* - 12.10 Quantal |
222| Mar 2010 | | | | 5.4 |
223| Apr 2010 | | | | 5.5 | 10.10 Maverick |
224| Apr 2010 | *Apr 2015* | | | | *10.04 Lucid* | 1.7.0.4 | 2.6.5 3.1.2 | 5.3 |
225| Jul 2010 | *Dec 2019* | | *2.7.0* | | 11.04 Natty - *<current>* |
226| Aug 2010 | | | | 5.6 |
227| Oct 2010 | | | | | 10.10 Maverick | 1.7.1 | 2.6.6 3.1.3 | 5.5 |
228| Jan 2011 | | | | 5.7 |
229| Feb 2011 | | | | 5.8 | 11.04 Natty |
230| Feb 2011 | *Feb 2016* | | | | Debian 6 Squeeze | 1.7.2.5 | 2.6.6 3.1.3 |
231| Apr 2011 | | | | | 11.04 Natty | 1.7.4 | 2.7.1 3.2.0 | 5.8 |
232| Sep 2011 | | | | 5.9 | *12.04 Precise* |
233| Oct 2011 | *Feb 2016* | | 3.2.0 | | 11.04 Natty - 12.10 Quantal |
234| Oct 2011 | | | | | 11.10 Ocelot | 1.7.5.4 | 2.7.2 3.2.2 | 5.8 |
235| Apr 2012 | | | | 6.0 | 12.10 Quantal |
236| Apr 2012 | *Apr 2019* | | | | *12.04 Precise* | 1.7.9.5 | 2.7.3 3.2.3 | 5.9 |
237| Aug 2012 | | | | 6.1 | 13.04 Raring |
238| Sep 2012 | *Sep 2017* | | 3.3.0 | | 13.04 Raring - 13.10 Saucy |
239| Oct 2012 | *Dec 2014* | 1.8.0 | | | 13.04 Raring - 13.10 Saucy |
240| Oct 2012 | | | | | 12.10 Quantal | 1.7.10.4 | 2.7.3 3.2.3 | 6.0 |
241| Mar 2013 | | | | 6.2 | 13.10 Saucy |
242| Apr 2013 | | | | | 13.04 Raring | 1.8.1.2 | 2.7.4 3.3.1 | 6.1 |
243| May 2013 | *May 2018* | | | | Debian 7 Wheezy | 1.7.10.4 | 2.7.3 3.2.3 |
244| Sep 2013 | | | | 6.3 |
245| Oct 2013 | | | | | 13.10 Saucy | 1.8.3.2 | 2.7.5 3.3.2 | 6.2 |
246| Nov 2013 | | | | 6.4 |
247| Jan 2014 | | | | 6.5 |
248| Feb 2014 | *Dec 2014* | **1.9.0** | | | *14.04 Trusty* |
249| Mar 2014 | *Mar 2019* | | *3.4.0* | | *14.04 Trusty* - 15.10 Wily / *Jessie* |
250| Mar 2014 | | | | 6.6 | *14.04 Trusty* - 14.10 Utopic |
251| Apr 2014 | *Apr 2022* | | | | *14.04 Trusty* | 1.9.1 | 2.7.5 3.4.0 | 6.6 |
252| May 2014 | *Dec 2014* | 2.0.0 |
253| Aug 2014 | *Dec 2014* | *2.1.0* | | | 14.10 Utopic - 15.04 Vivid / *Jessie* |
254| Oct 2014 | | | | 6.7 | 15.04 Vivid |
255| Oct 2014 | | | | | 14.10 Utopic | 2.1.0 | 2.7.8 3.4.2 | 6.6 |
256| Nov 2014 | *Sep 2015* | 2.2.0 |
257| Feb 2015 | *Sep 2015* | 2.3.0 |
258| Mar 2015 | | | | 6.8 |
259| Apr 2015 | *May 2017* | 2.4.0 |
260| Apr 2015 | *Jun 2020* | | | | *Debian 8 Jessie* | 2.1.4 | 2.7.9 3.4.2 |
261| Apr 2015 | | | | | 15.04 Vivid | 2.1.4 | 2.7.9 3.4.3 | 6.7 |
262| Jul 2015 | *May 2017* | 2.5.0 | | | 15.10 Wily |
263| Jul 2015 | | | | 6.9 | 15.10 Wily |
264| Aug 2015 | | | | 7.0 |
265| Aug 2015 | | | | 7.1 |
266| Sep 2015 | *May 2017* | 2.6.0 |
267| Sep 2015 | *Sep 2020* | | *3.5.0* | | *16.04 Xenial* - 17.04 Zesty / *Stretch* |
268| Oct 2015 | | | | | 15.10 Wily | 2.5.0 | 2.7.9 3.4.3 | 6.9 |
269| Jan 2016 | *Jul 2017* | *2.7.0* | | | *16.04 Xenial* |
270| Feb 2016 | | | | 7.2 | *16.04 Xenial* |
271| Mar 2016 | *Jul 2017* | 2.8.0 |
272| Apr 2016 | *Apr 2024* | | | | *16.04 Xenial* | 2.7.4 | 2.7.11 3.5.1 | 7.2 |
273| Jun 2016 | *Jul 2017* | 2.9.0 | | | 16.10 Yakkety |
274| Jul 2016 | | | | 7.3 | 16.10 Yakkety |
275| Sep 2016 | *Sep 2017* | 2.10.0 |
276| Oct 2016 | | | | | 16.10 Yakkety | 2.9.3 | 2.7.11 3.5.1 | 7.3 |
277| Nov 2016 | *Sep 2017* | *2.11.0* | | | 17.04 Zesty / *Stretch* |
278| Dec 2016 | **Dec 2021** | | **3.6.0** | | 17.10 Artful - **18.04 Bionic** - 18.10 Cosmic |
279| Dec 2016 | | | | 7.4 | 17.04 Zesty / *Debian 9 Stretch* |
280| Feb 2017 | *Sep 2017* | 2.12.0 |
281| Mar 2017 | | | | 7.5 | 17.10 Artful |
282| Apr 2017 | | | | | 17.04 Zesty | 2.11.0 | 2.7.13 3.5.3 | 7.4 |
283| May 2017 | *May 2018* | 2.13.0 |
284| Jun 2017 | *Jun 2022* | | | | *Debian 9 Stretch* | 2.11.0 | 2.7.13 3.5.3 | 7.4 |
285| Aug 2017 | *Dec 2019* | 2.14.0 | | | 17.10 Artful |
286| Oct 2017 | *Dec 2019* | 2.15.0 |
287| Oct 2017 | | | | 7.6 | **18.04 Bionic** |
288| Oct 2017 | | | | | 17.10 Artful | 2.14.1 | 2.7.14 3.6.3 | 7.5 |
289| Jan 2018 | *Dec 2019* | 2.16.0 |
290| Apr 2018 | *Mar 2021* | **2.17.0** | | | **18.04 Bionic** |
291| Apr 2018 | | | | 7.7 | 18.10 Cosmic |
292| Apr 2018 | **Apr 2028** | | | | **18.04 Bionic** | 2.17.0 | 2.7.15 3.6.5 | 7.6 |
293| Jun 2018 | *Mar 2021* | 2.18.0 |
294| Jun 2018 | **Jun 2023** | | 3.7.0 | | 19.04 Disco - **20.04 Focal** / **Buster** |
295| Aug 2018 | | | | 7.8 |
296| Sep 2018 | *Mar 2021* | 2.19.0 | | | 18.10 Cosmic |
297| Oct 2018 | | | | 7.9 | 19.04 Disco / **Buster** |
298| Oct 2018 | | | | | 18.10 Cosmic | 2.19.1 | 2.7.15 3.6.6 | 7.7 |
299| Dec 2018 | *Mar 2021* | **2.20.0** | | | 19.04 Disco - 19.10 Eoan / **Buster** |
300| Feb 2019 | *Mar 2021* | 2.21.0 |
301| Apr 2019 | | | | 8.0 | 19.10 Eoan |
302| Apr 2019 | | | | | 19.04 Disco | 2.20.1 | 2.7.16 3.7.3 | 7.9 |
303| Jun 2019 | | 2.22.0 |
304| Jul 2019 | **Jul 2024** | | | | **Debian 10 Buster** | 2.20.1 | 2.7.16 3.7.3 | 7.9 |
305| Aug 2019 | *Mar 2021* | 2.23.0 |
306| Oct 2019 | **Oct 2024** | | 3.8.0 | | **20.04 Focal** - 20.10 Groovy |
307| Oct 2019 | | | | 8.1 |
308| Oct 2019 | | | | | 19.10 Eoan | 2.20.1 | 2.7.17 3.7.5 | 8.0 |
309| Nov 2019 | *Mar 2021* | 2.24.0 |
310| Jan 2020 | *Mar 2021* | 2.25.0 | | | **20.04 Focal** |
311| Feb 2020 | | | | 8.2 | **20.04 Focal** |
312| Mar 2020 | *Mar 2021* | 2.26.0 |
313| Apr 2020 | **Apr 2030** | | | | **20.04 Focal** | 2.25.1 | 2.7.17 3.8.2 | 8.2 |
314| May 2020 | *Mar 2021* | 2.27.0 | | | 20.10 Groovy |
315| May 2020 | | | | 8.3 |
316| Jul 2020 | *Mar 2021* | 2.28.0 |
317| Sep 2020 | | | | 8.4 | 21.04 Hirsute / **Bullseye** |
318| Oct 2020 | *Mar 2021* | 2.29.0 |
319| Oct 2020 | | | | | 20.10 Groovy | 2.27.0 | 2.7.18 3.8.6 | 8.3 |
320| Oct 2020 | **Oct 2025** | | 3.9.0 | | 21.04 Hirsute / **Bullseye** |
321| Dec 2020 | *Mar 2021* | 2.30.0 | | | 21.04 Hirsute / **Bullseye** |
322| Mar 2021 | | 2.31.0 |
323| Mar 2021 | | | | 8.5 |
324| Apr 2021 | | | | 8.6 |
325| Apr 2021 | *Jan 2022* | | | | 21.04 Hirsute | 2.30.2 | 2.7.18 3.9.4 | 8.4 |
326| Jun 2021 | | 2.32.0 |
327| Aug 2021 | | 2.33.0 |
328| Aug 2021 | | | | 8.7 |
329| Aug 2021 | **Aug 2026** | | | | **Debian 11 Bullseye** | 2.30.2 | 2.7.18 3.9.2 | 8.4 |
330| **Date** | **EOL** | **[Git][rel-g]** | **[Python][rel-p]** | **[SSH][rel-o]** | **[Ubuntu][rel-u] / [Debian][rel-d]** | **Git** | **Python** | **SSH** |
331
332
333[contact]: ../README.md#contact
334[rel-d]: https://en.wikipedia.org/wiki/Debian_version_history
335[rel-g]: https://en.wikipedia.org/wiki/Git#Releases
336[rel-o]: https://www.openssh.com/releasenotes.html
337[rel-p]: https://en.wikipedia.org/wiki/History_of_Python#Table_of_versions
338[rel-u]: https://en.wikipedia.org/wiki/Ubuntu_version_history#Table_of_versions
165[example announcement]: https://groups.google.com/d/topic/repo-discuss/UGBNismWo1M/discussion 339[example announcement]: https://groups.google.com/d/topic/repo-discuss/UGBNismWo1M/discussion
166[repo-discuss@googlegroups.com]: https://groups.google.com/forum/#!forum/repo-discuss 340[repo-discuss@googlegroups.com]: https://groups.google.com/forum/#!forum/repo-discuss
167[go/repo-release]: https://goto.google.com/repo-release 341[go/repo-release]: https://goto.google.com/repo-release
diff --git a/docs/repo-hooks.md b/docs/repo-hooks.md
index 7c37c30e..cbb1aac7 100644
--- a/docs/repo-hooks.md
+++ b/docs/repo-hooks.md
@@ -27,7 +27,7 @@ repohooks project is updated and a hook is triggered.
27For the full syntax, see the [repo manifest format](./manifest-format.md). 27For the full syntax, see the [repo manifest format](./manifest-format.md).
28 28
29Here's a short example from 29Here's a short example from
30[Android](https://android.googlesource.com/platform/manifest/+/master/default.xml). 30[Android](https://android.googlesource.com/platform/manifest/+/HEAD/default.xml).
31The `<project>` line checks out the repohooks git repo to the local 31The `<project>` line checks out the repohooks git repo to the local
32`tools/repohooks/` path. The `<repo-hooks>` line says to look in the project 32`tools/repohooks/` path. The `<repo-hooks>` line says to look in the project
33with the name `platform/tools/repohooks` for hooks to run during the 33with the name `platform/tools/repohooks` for hooks to run during the
diff --git a/docs/windows.md b/docs/windows.md
index 80912964..4282bebf 100644
--- a/docs/windows.md
+++ b/docs/windows.md
@@ -19,7 +19,33 @@ also due to most developers not using Windows.
19We will never add code specific to older versions of Windows. 19We will never add code specific to older versions of Windows.
20It might work, but it most likely won't, so please don't bother asking. 20It might work, but it most likely won't, so please don't bother asking.
21 21
22## Symlinks 22## Git worktrees
23
24*** note
25**Warning**: Repo's support for Git worktrees is new & experimental.
26Please report any bugs and be sure to maintain backups!
27***
28
29The Repo 2.4 release introduced support for [Git worktrees][git-worktree].
30You don't have to worry about or understand this particular feature, so don't
31worry if this section of the Git manual is particularly impenetrable.
32
33The salient point is that Git worktrees allow Repo to create repo client
34checkouts that do not require symlinks at all under Windows.
35This means users no longer need Administrator access to sync code.
36
37Simply use `--worktree` when running `repo init` to opt in.
38
39This does not effect specific Git repositories that use symlinks themselves.
40
41[git-worktree]: https://git-scm.com/docs/git-worktree
42
43## Symlinks by default
44
45*** note
46**NB**: This section applies to the default Repo behavior which does not use
47Git worktrees (see the previous section for more info).
48***
23 49
24Repo will use symlinks heavily internally. 50Repo will use symlinks heavily internally.
25On *NIX platforms, this isn't an issue, but Windows makes it a bit difficult. 51On *NIX platforms, this isn't an issue, but Windows makes it a bit difficult.
@@ -62,9 +88,8 @@ This also helps `tar` unpack symlinks, so that's nice.
62 88
63## Python 89## Python
64 90
65You should make sure to be running Python 3.6 or newer under Windows. 91Python 3.6 or newer is required.
66Python 2 might work, but due to already limited platform testing, you should 92Python 2 is known to be broken when running under Windows.
67only run newer Python versions.
68See our [Python Support](./python-support.md) document for more details. 93See our [Python Support](./python-support.md) document for more details.
69 94
70You can grab the latest Windows installer here:<br> 95You can grab the latest Windows installer here:<br>
diff --git a/editor.py b/editor.py
index fcf16386..b84a42d4 100644
--- a/editor.py
+++ b/editor.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,7 +12,6 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function
18import os 15import os
19import re 16import re
20import sys 17import sys
@@ -24,6 +21,7 @@ import tempfile
24from error import EditorError 21from error import EditorError
25import platform_utils 22import platform_utils
26 23
24
27class Editor(object): 25class Editor(object):
28 """Manages the user's preferred text editor.""" 26 """Manages the user's preferred text editor."""
29 27
@@ -57,7 +55,7 @@ class Editor(object):
57 55
58 if os.getenv('TERM') == 'dumb': 56 if os.getenv('TERM') == 'dumb':
59 print( 57 print(
60"""No editor specified in GIT_EDITOR, core.editor, VISUAL or EDITOR. 58 """No editor specified in GIT_EDITOR, core.editor, VISUAL or EDITOR.
61Tried to fall back to vi but terminal is dumb. Please configure at 59Tried to fall back to vi but terminal is dumb. Please configure at
62least one of these before using this command.""", file=sys.stderr) 60least one of these before using this command.""", file=sys.stderr)
63 sys.exit(1) 61 sys.exit(1)
@@ -104,10 +102,10 @@ least one of these before using this command.""", file=sys.stderr)
104 rc = subprocess.Popen(args, shell=shell).wait() 102 rc = subprocess.Popen(args, shell=shell).wait()
105 except OSError as e: 103 except OSError as e:
106 raise EditorError('editor failed, %s: %s %s' 104 raise EditorError('editor failed, %s: %s %s'
107 % (str(e), editor, path)) 105 % (str(e), editor, path))
108 if rc != 0: 106 if rc != 0:
109 raise EditorError('editor failed with exit status %d: %s %s' 107 raise EditorError('editor failed with exit status %d: %s %s'
110 % (rc, editor, path)) 108 % (rc, editor, path))
111 109
112 with open(path, mode='rb') as fd2: 110 with open(path, mode='rb') as fd2:
113 return fd2.read().decode('utf-8') 111 return fd2.read().decode('utf-8')
diff --git a/error.py b/error.py
index 5bfe3a66..cbefcb7e 100644
--- a/error.py
+++ b/error.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,70 +12,89 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
15
17class ManifestParseError(Exception): 16class ManifestParseError(Exception):
18 """Failed to parse the manifest file. 17 """Failed to parse the manifest file.
19 """ 18 """
20 19
21class ManifestInvalidRevisionError(Exception): 20
21class ManifestInvalidRevisionError(ManifestParseError):
22 """The revision value in a project is incorrect. 22 """The revision value in a project is incorrect.
23 """ 23 """
24 24
25
26class ManifestInvalidPathError(ManifestParseError):
27 """A path used in <copyfile> or <linkfile> is incorrect.
28 """
29
30
25class NoManifestException(Exception): 31class NoManifestException(Exception):
26 """The required manifest does not exist. 32 """The required manifest does not exist.
27 """ 33 """
34
28 def __init__(self, path, reason): 35 def __init__(self, path, reason):
29 super(NoManifestException, self).__init__() 36 super().__init__(path, reason)
30 self.path = path 37 self.path = path
31 self.reason = reason 38 self.reason = reason
32 39
33 def __str__(self): 40 def __str__(self):
34 return self.reason 41 return self.reason
35 42
43
36class EditorError(Exception): 44class EditorError(Exception):
37 """Unspecified error from the user's text editor. 45 """Unspecified error from the user's text editor.
38 """ 46 """
47
39 def __init__(self, reason): 48 def __init__(self, reason):
40 super(EditorError, self).__init__() 49 super().__init__(reason)
41 self.reason = reason 50 self.reason = reason
42 51
43 def __str__(self): 52 def __str__(self):
44 return self.reason 53 return self.reason
45 54
55
46class GitError(Exception): 56class GitError(Exception):
47 """Unspecified internal error from git. 57 """Unspecified internal error from git.
48 """ 58 """
59
49 def __init__(self, command): 60 def __init__(self, command):
50 super(GitError, self).__init__() 61 super().__init__(command)
51 self.command = command 62 self.command = command
52 63
53 def __str__(self): 64 def __str__(self):
54 return self.command 65 return self.command
55 66
67
56class UploadError(Exception): 68class UploadError(Exception):
57 """A bundle upload to Gerrit did not succeed. 69 """A bundle upload to Gerrit did not succeed.
58 """ 70 """
71
59 def __init__(self, reason): 72 def __init__(self, reason):
60 super(UploadError, self).__init__() 73 super().__init__(reason)
61 self.reason = reason 74 self.reason = reason
62 75
63 def __str__(self): 76 def __str__(self):
64 return self.reason 77 return self.reason
65 78
79
66class DownloadError(Exception): 80class DownloadError(Exception):
67 """Cannot download a repository. 81 """Cannot download a repository.
68 """ 82 """
83
69 def __init__(self, reason): 84 def __init__(self, reason):
70 super(DownloadError, self).__init__() 85 super().__init__(reason)
71 self.reason = reason 86 self.reason = reason
72 87
73 def __str__(self): 88 def __str__(self):
74 return self.reason 89 return self.reason
75 90
91
76class NoSuchProjectError(Exception): 92class NoSuchProjectError(Exception):
77 """A specified project does not exist in the work tree. 93 """A specified project does not exist in the work tree.
78 """ 94 """
95
79 def __init__(self, name=None): 96 def __init__(self, name=None):
80 super(NoSuchProjectError, self).__init__() 97 super().__init__(name)
81 self.name = name 98 self.name = name
82 99
83 def __str__(self): 100 def __str__(self):
@@ -89,8 +106,9 @@ class NoSuchProjectError(Exception):
89class InvalidProjectGroupsError(Exception): 106class InvalidProjectGroupsError(Exception):
90 """A specified project is not suitable for the specified groups 107 """A specified project is not suitable for the specified groups
91 """ 108 """
109
92 def __init__(self, name=None): 110 def __init__(self, name=None):
93 super(InvalidProjectGroupsError, self).__init__() 111 super().__init__(name)
94 self.name = name 112 self.name = name
95 113
96 def __str__(self): 114 def __str__(self):
@@ -98,15 +116,18 @@ class InvalidProjectGroupsError(Exception):
98 return 'in current directory' 116 return 'in current directory'
99 return self.name 117 return self.name
100 118
119
101class RepoChangedException(Exception): 120class RepoChangedException(Exception):
102 """Thrown if 'repo sync' results in repo updating its internal 121 """Thrown if 'repo sync' results in repo updating its internal
103 repo or manifest repositories. In this special case we must 122 repo or manifest repositories. In this special case we must
104 use exec to re-execute repo with the new code and manifest. 123 use exec to re-execute repo with the new code and manifest.
105 """ 124 """
125
106 def __init__(self, extra_args=None): 126 def __init__(self, extra_args=None):
107 super(RepoChangedException, self).__init__() 127 super().__init__(extra_args)
108 self.extra_args = extra_args or [] 128 self.extra_args = extra_args or []
109 129
130
110class HookError(Exception): 131class HookError(Exception):
111 """Thrown if a 'repo-hook' could not be run. 132 """Thrown if a 'repo-hook' could not be run.
112 133
diff --git a/event_log.py b/event_log.py
index 315d752d..c77c5648 100644
--- a/event_log.py
+++ b/event_log.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2017 The Android Open Source Project 1# Copyright (C) 2017 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,8 +12,6 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function
18
19import json 15import json
20import multiprocessing 16import multiprocessing
21 17
@@ -23,6 +19,7 @@ TASK_COMMAND = 'command'
23TASK_SYNC_NETWORK = 'sync-network' 19TASK_SYNC_NETWORK = 'sync-network'
24TASK_SYNC_LOCAL = 'sync-local' 20TASK_SYNC_LOCAL = 'sync-local'
25 21
22
26class EventLog(object): 23class EventLog(object):
27 """Event log that records events that occurred during a repo invocation. 24 """Event log that records events that occurred during a repo invocation.
28 25
@@ -138,7 +135,7 @@ class EventLog(object):
138 Returns: 135 Returns:
139 A dictionary of the event added to the log. 136 A dictionary of the event added to the log.
140 """ 137 """
141 event['status'] = self.GetStatusString(success) 138 event['status'] = self.GetStatusString(success)
142 event['finish_time'] = finish 139 event['finish_time'] = finish
143 return event 140 return event
144 141
@@ -165,6 +162,7 @@ class EventLog(object):
165# An integer id that is unique across this invocation of the program. 162# An integer id that is unique across this invocation of the program.
166_EVENT_ID = multiprocessing.Value('i', 1) 163_EVENT_ID = multiprocessing.Value('i', 1)
167 164
165
168def _NextEventId(): 166def _NextEventId():
169 """Helper function for grabbing the next unique id. 167 """Helper function for grabbing the next unique id.
170 168
diff --git a/fetch.py b/fetch.py
new file mode 100644
index 00000000..91d40cda
--- /dev/null
+++ b/fetch.py
@@ -0,0 +1,41 @@
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
17import subprocess
18import sys
19from urllib.parse import urlparse
20
21def fetch_file(url):
22 """Fetch a file from the specified source using the appropriate protocol.
23
24 Returns:
25 The contents of the file as bytes.
26 """
27 scheme = urlparse(url).scheme
28 if scheme == 'gs':
29 cmd = ['gsutil', 'cat', url]
30 try:
31 result = subprocess.run(
32 cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
33 return result.stdout
34 except subprocess.CalledProcessError as e:
35 print('fatal: error running "gsutil": %s' % e.output,
36 file=sys.stderr)
37 sys.exit(1)
38 if scheme == 'file':
39 with open(url[len('file://'):], 'rb') as f:
40 return f.read()
41 raise ValueError('unsupported url %s' % url)
diff --git a/git_command.py b/git_command.py
index dc542c36..95db91f2 100644
--- a/git_command.py
+++ b/git_command.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,12 +12,10 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function 15import functools
18import os 16import os
19import sys 17import sys
20import subprocess 18import subprocess
21import tempfile
22from signal import SIGTERM
23 19
24from error import GitError 20from error import GitError
25from git_refs import HEAD 21from git_refs import HEAD
@@ -28,75 +24,42 @@ from repo_trace import REPO_TRACE, IsTrace, Trace
28from wrapper import Wrapper 24from wrapper import Wrapper
29 25
30GIT = 'git' 26GIT = 'git'
31MIN_GIT_VERSION = (1, 5, 4) 27# NB: These do not need to be kept in sync with the repo launcher script.
28# These may be much newer as it allows the repo launcher to roll between
29# different repo releases while source versions might require a newer git.
30#
31# The soft version is when we start warning users that the version is old and
32# we'll be dropping support for it. We'll refuse to work with versions older
33# than the hard version.
34#
35# git-1.7 is in (EOL) Ubuntu Precise. git-1.9 is in Ubuntu Trusty.
36MIN_GIT_VERSION_SOFT = (1, 9, 1)
37MIN_GIT_VERSION_HARD = (1, 7, 2)
32GIT_DIR = 'GIT_DIR' 38GIT_DIR = 'GIT_DIR'
33 39
34LAST_GITDIR = None 40LAST_GITDIR = None
35LAST_CWD = None 41LAST_CWD = None
36 42
37_ssh_proxy_path = None
38_ssh_sock_path = None
39_ssh_clients = []
40
41def ssh_sock(create=True):
42 global _ssh_sock_path
43 if _ssh_sock_path is None:
44 if not create:
45 return None
46 tmp_dir = '/tmp'
47 if not os.path.exists(tmp_dir):
48 tmp_dir = tempfile.gettempdir()
49 _ssh_sock_path = os.path.join(
50 tempfile.mkdtemp('', 'ssh-', tmp_dir),
51 'master-%r@%h:%p')
52 return _ssh_sock_path
53
54def _ssh_proxy():
55 global _ssh_proxy_path
56 if _ssh_proxy_path is None:
57 _ssh_proxy_path = os.path.join(
58 os.path.dirname(__file__),
59 'git_ssh')
60 return _ssh_proxy_path
61
62def _add_ssh_client(p):
63 _ssh_clients.append(p)
64
65def _remove_ssh_client(p):
66 try:
67 _ssh_clients.remove(p)
68 except ValueError:
69 pass
70
71def terminate_ssh_clients():
72 global _ssh_clients
73 for p in _ssh_clients:
74 try:
75 os.kill(p.pid, SIGTERM)
76 p.wait()
77 except OSError:
78 pass
79 _ssh_clients = []
80
81_git_version = None
82 43
83class _GitCall(object): 44class _GitCall(object):
45 @functools.lru_cache(maxsize=None)
84 def version_tuple(self): 46 def version_tuple(self):
85 global _git_version 47 ret = Wrapper().ParseGitVersion()
86 if _git_version is None: 48 if ret is None:
87 _git_version = Wrapper().ParseGitVersion() 49 print('fatal: unable to detect git version', file=sys.stderr)
88 if _git_version is None: 50 sys.exit(1)
89 print('fatal: unable to detect git version', file=sys.stderr) 51 return ret
90 sys.exit(1)
91 return _git_version
92 52
93 def __getattr__(self, name): 53 def __getattr__(self, name):
94 name = name.replace('_','-') 54 name = name.replace('_', '-')
55
95 def fun(*cmdv): 56 def fun(*cmdv):
96 command = [name] 57 command = [name]
97 command.extend(cmdv) 58 command.extend(cmdv)
98 return GitCommand(None, command).Wait() == 0 59 return GitCommand(None, command).Wait() == 0
99 return fun 60 return fun
61
62
100git = _GitCall() 63git = _GitCall()
101 64
102 65
@@ -111,11 +74,11 @@ def RepoSourceVersion():
111 74
112 proj = os.path.dirname(os.path.abspath(__file__)) 75 proj = os.path.dirname(os.path.abspath(__file__))
113 env[GIT_DIR] = os.path.join(proj, '.git') 76 env[GIT_DIR] = os.path.join(proj, '.git')
114 77 result = subprocess.run([GIT, 'describe', HEAD], stdout=subprocess.PIPE,
115 p = subprocess.Popen([GIT, 'describe', HEAD], stdout=subprocess.PIPE, 78 stderr=subprocess.DEVNULL, encoding='utf-8',
116 env=env) 79 env=env, check=False)
117 if p.wait() == 0: 80 if result.returncode == 0:
118 ver = p.stdout.read().strip().decode('utf-8') 81 ver = result.stdout.strip()
119 if ver.startswith('v'): 82 if ver.startswith('v'):
120 ver = ver[1:] 83 ver = ver[1:]
121 else: 84 else:
@@ -177,8 +140,10 @@ class UserAgent(object):
177 140
178 return self._git_ua 141 return self._git_ua
179 142
143
180user_agent = UserAgent() 144user_agent = UserAgent()
181 145
146
182def git_require(min_version, fail=False, msg=''): 147def git_require(min_version, fail=False, msg=''):
183 git_version = git.version_tuple() 148 git_version = git.version_tuple()
184 if min_version <= git_version: 149 if min_version <= git_version:
@@ -191,42 +156,38 @@ def git_require(min_version, fail=False, msg=''):
191 sys.exit(1) 156 sys.exit(1)
192 return False 157 return False
193 158
194def _setenv(env, name, value):
195 env[name] = value.encode()
196 159
197class GitCommand(object): 160class GitCommand(object):
198 def __init__(self, 161 def __init__(self,
199 project, 162 project,
200 cmdv, 163 cmdv,
201 bare = False, 164 bare=False,
202 provide_stdin = False, 165 input=None,
203 capture_stdout = False, 166 capture_stdout=False,
204 capture_stderr = False, 167 capture_stderr=False,
205 disable_editor = False, 168 merge_output=False,
206 ssh_proxy = False, 169 disable_editor=False,
207 cwd = None, 170 ssh_proxy=None,
208 gitdir = None): 171 cwd=None,
172 gitdir=None):
209 env = self._GetBasicEnv() 173 env = self._GetBasicEnv()
210 174
211 # If we are not capturing std* then need to print it.
212 self.tee = {'stdout': not capture_stdout, 'stderr': not capture_stderr}
213
214 if disable_editor: 175 if disable_editor:
215 _setenv(env, 'GIT_EDITOR', ':') 176 env['GIT_EDITOR'] = ':'
216 if ssh_proxy: 177 if ssh_proxy:
217 _setenv(env, 'REPO_SSH_SOCK', ssh_sock()) 178 env['REPO_SSH_SOCK'] = ssh_proxy.sock()
218 _setenv(env, 'GIT_SSH', _ssh_proxy()) 179 env['GIT_SSH'] = ssh_proxy.proxy
219 _setenv(env, 'GIT_SSH_VARIANT', 'ssh') 180 env['GIT_SSH_VARIANT'] = 'ssh'
220 if 'http_proxy' in env and 'darwin' == sys.platform: 181 if 'http_proxy' in env and 'darwin' == sys.platform:
221 s = "'http.proxy=%s'" % (env['http_proxy'],) 182 s = "'http.proxy=%s'" % (env['http_proxy'],)
222 p = env.get('GIT_CONFIG_PARAMETERS') 183 p = env.get('GIT_CONFIG_PARAMETERS')
223 if p is not None: 184 if p is not None:
224 s = p + ' ' + s 185 s = p + ' ' + s
225 _setenv(env, 'GIT_CONFIG_PARAMETERS', s) 186 env['GIT_CONFIG_PARAMETERS'] = s
226 if 'GIT_ALLOW_PROTOCOL' not in env: 187 if 'GIT_ALLOW_PROTOCOL' not in env:
227 _setenv(env, 'GIT_ALLOW_PROTOCOL', 188 env['GIT_ALLOW_PROTOCOL'] = (
228 'file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc') 189 'file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc')
229 _setenv(env, 'GIT_HTTP_USER_AGENT', user_agent.git) 190 env['GIT_HTTP_USER_AGENT'] = user_agent.git
230 191
231 if project: 192 if project:
232 if not cwd: 193 if not cwd:
@@ -237,7 +198,10 @@ class GitCommand(object):
237 command = [GIT] 198 command = [GIT]
238 if bare: 199 if bare:
239 if gitdir: 200 if gitdir:
240 _setenv(env, GIT_DIR, gitdir) 201 # Git on Windows wants its paths only using / for reliability.
202 if platform_utils.isWindows():
203 gitdir = gitdir.replace('\\', '/')
204 env[GIT_DIR] = gitdir
241 cwd = None 205 cwd = None
242 command.append(cmdv[0]) 206 command.append(cmdv[0])
243 # Need to use the --progress flag for fetch/clone so output will be 207 # Need to use the --progress flag for fetch/clone so output will be
@@ -247,13 +211,10 @@ class GitCommand(object):
247 command.append('--progress') 211 command.append('--progress')
248 command.extend(cmdv[1:]) 212 command.extend(cmdv[1:])
249 213
250 if provide_stdin: 214 stdin = subprocess.PIPE if input else None
251 stdin = subprocess.PIPE 215 stdout = subprocess.PIPE if capture_stdout else None
252 else: 216 stderr = (subprocess.STDOUT if merge_output else
253 stdin = None 217 (subprocess.PIPE if capture_stderr else None))
254
255 stdout = subprocess.PIPE
256 stderr = subprocess.PIPE
257 218
258 if IsTrace(): 219 if IsTrace():
259 global LAST_CWD 220 global LAST_CWD
@@ -281,23 +242,38 @@ class GitCommand(object):
281 dbg += ' 1>|' 242 dbg += ' 1>|'
282 if stderr == subprocess.PIPE: 243 if stderr == subprocess.PIPE:
283 dbg += ' 2>|' 244 dbg += ' 2>|'
245 elif stderr == subprocess.STDOUT:
246 dbg += ' 2>&1'
284 Trace('%s', dbg) 247 Trace('%s', dbg)
285 248
286 try: 249 try:
287 p = subprocess.Popen(command, 250 p = subprocess.Popen(command,
288 cwd = cwd, 251 cwd=cwd,
289 env = env, 252 env=env,
290 stdin = stdin, 253 encoding='utf-8',
291 stdout = stdout, 254 errors='backslashreplace',
292 stderr = stderr) 255 stdin=stdin,
256 stdout=stdout,
257 stderr=stderr)
293 except Exception as e: 258 except Exception as e:
294 raise GitError('%s: %s' % (command[1], e)) 259 raise GitError('%s: %s' % (command[1], e))
295 260
296 if ssh_proxy: 261 if ssh_proxy:
297 _add_ssh_client(p) 262 ssh_proxy.add_client(p)
298 263
299 self.process = p 264 self.process = p
300 self.stdin = p.stdin 265 if input:
266 if isinstance(input, str):
267 input = input.encode('utf-8')
268 p.stdin.write(input)
269 p.stdin.close()
270
271 try:
272 self.stdout, self.stderr = p.communicate()
273 finally:
274 if ssh_proxy:
275 ssh_proxy.remove_client(p)
276 self.rc = p.wait()
301 277
302 @staticmethod 278 @staticmethod
303 def _GetBasicEnv(): 279 def _GetBasicEnv():
@@ -317,35 +293,4 @@ class GitCommand(object):
317 return env 293 return env
318 294
319 def Wait(self): 295 def Wait(self):
320 try: 296 return self.rc
321 p = self.process
322 rc = self._CaptureOutput()
323 finally:
324 _remove_ssh_client(p)
325 return rc
326
327 def _CaptureOutput(self):
328 p = self.process
329 s_in = platform_utils.FileDescriptorStreams.create()
330 s_in.add(p.stdout, sys.stdout, 'stdout')
331 s_in.add(p.stderr, sys.stderr, 'stderr')
332 self.stdout = ''
333 self.stderr = ''
334
335 while not s_in.is_done:
336 in_ready = s_in.select()
337 for s in in_ready:
338 buf = s.read()
339 if not buf:
340 s_in.remove(s)
341 continue
342 if not hasattr(buf, 'encode'):
343 buf = buf.decode()
344 if s.std_name == 'stdout':
345 self.stdout += buf
346 else:
347 self.stderr += buf
348 if self.tee[s.std_name]:
349 s.dest.write(buf)
350 s.dest.flush()
351 return p.wait()
diff --git a/git_config.py b/git_config.py
index 8de3200c..3cd09391 100644
--- a/git_config.py
+++ b/git_config.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,84 +12,83 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function
18
19import contextlib 15import contextlib
16import datetime
20import errno 17import errno
18from http.client import HTTPException
21import json 19import json
22import os 20import os
23import re 21import re
24import ssl 22import ssl
25import subprocess 23import subprocess
26import sys 24import sys
27try: 25import urllib.error
28 import threading as _threading 26import urllib.request
29except ImportError: 27
30 import dummy_threading as _threading
31import time
32
33from pyversion import is_python3
34if is_python3():
35 import urllib.request
36 import urllib.error
37else:
38 import urllib2
39 import imp
40 urllib = imp.new_module('urllib')
41 urllib.request = urllib2
42 urllib.error = urllib2
43
44from signal import SIGTERM
45from error import GitError, UploadError 28from error import GitError, UploadError
46import platform_utils 29import platform_utils
47from repo_trace import Trace 30from repo_trace import Trace
48if is_python3():
49 from http.client import HTTPException
50else:
51 from httplib import HTTPException
52
53from git_command import GitCommand 31from git_command import GitCommand
54from git_command import ssh_sock
55from git_command import terminate_ssh_clients
56from git_refs import R_CHANGES, R_HEADS, R_TAGS 32from git_refs import R_CHANGES, R_HEADS, R_TAGS
57 33
34# Prefix that is prepended to all the keys of SyncAnalysisState's data
35# that is saved in the config.
36SYNC_STATE_PREFIX = 'repo.syncstate.'
37
58ID_RE = re.compile(r'^[0-9a-f]{40}$') 38ID_RE = re.compile(r'^[0-9a-f]{40}$')
59 39
60REVIEW_CACHE = dict() 40REVIEW_CACHE = dict()
61 41
42
62def IsChange(rev): 43def IsChange(rev):
63 return rev.startswith(R_CHANGES) 44 return rev.startswith(R_CHANGES)
64 45
46
65def IsId(rev): 47def IsId(rev):
66 return ID_RE.match(rev) 48 return ID_RE.match(rev)
67 49
50
68def IsTag(rev): 51def IsTag(rev):
69 return rev.startswith(R_TAGS) 52 return rev.startswith(R_TAGS)
70 53
54
71def IsImmutable(rev): 55def IsImmutable(rev):
72 return IsChange(rev) or IsId(rev) or IsTag(rev) 56 return IsChange(rev) or IsId(rev) or IsTag(rev)
73 57
58
74def _key(name): 59def _key(name):
75 parts = name.split('.') 60 parts = name.split('.')
76 if len(parts) < 2: 61 if len(parts) < 2:
77 return name.lower() 62 return name.lower()
78 parts[ 0] = parts[ 0].lower() 63 parts[0] = parts[0].lower()
79 parts[-1] = parts[-1].lower() 64 parts[-1] = parts[-1].lower()
80 return '.'.join(parts) 65 return '.'.join(parts)
81 66
67
82class GitConfig(object): 68class GitConfig(object):
83 _ForUser = None 69 _ForUser = None
84 70
71 _USER_CONFIG = '~/.gitconfig'
72
73 _ForSystem = None
74 _SYSTEM_CONFIG = '/etc/gitconfig'
75
76 @classmethod
77 def ForSystem(cls):
78 if cls._ForSystem is None:
79 cls._ForSystem = cls(configfile=cls._SYSTEM_CONFIG)
80 return cls._ForSystem
81
85 @classmethod 82 @classmethod
86 def ForUser(cls): 83 def ForUser(cls):
87 if cls._ForUser is None: 84 if cls._ForUser is None:
88 cls._ForUser = cls(configfile = os.path.expanduser('~/.gitconfig')) 85 cls._ForUser = cls(configfile=os.path.expanduser(cls._USER_CONFIG))
89 return cls._ForUser 86 return cls._ForUser
90 87
91 @classmethod 88 @classmethod
92 def ForRepository(cls, gitdir, defaults=None): 89 def ForRepository(cls, gitdir, defaults=None):
93 return cls(configfile = os.path.join(gitdir, 'config'), 90 return cls(configfile=os.path.join(gitdir, 'config'),
94 defaults = defaults) 91 defaults=defaults)
95 92
96 def __init__(self, configfile, defaults=None, jsonFile=None): 93 def __init__(self, configfile, defaults=None, jsonFile=None):
97 self.file = configfile 94 self.file = configfile
@@ -104,18 +101,74 @@ class GitConfig(object):
104 self._json = jsonFile 101 self._json = jsonFile
105 if self._json is None: 102 if self._json is None:
106 self._json = os.path.join( 103 self._json = os.path.join(
107 os.path.dirname(self.file), 104 os.path.dirname(self.file),
108 '.repo_' + os.path.basename(self.file) + '.json') 105 '.repo_' + os.path.basename(self.file) + '.json')
106
107 def ClearCache(self):
108 """Clear the in-memory cache of config."""
109 self._cache_dict = None
109 110
110 def Has(self, name, include_defaults = True): 111 def Has(self, name, include_defaults=True):
111 """Return true if this configuration file has the key. 112 """Return true if this configuration file has the key.
112 """ 113 """
113 if _key(name) in self._cache: 114 if _key(name) in self._cache:
114 return True 115 return True
115 if include_defaults and self.defaults: 116 if include_defaults and self.defaults:
116 return self.defaults.Has(name, include_defaults = True) 117 return self.defaults.Has(name, include_defaults=True)
117 return False 118 return False
118 119
120 def GetInt(self, name):
121 """Returns an integer from the configuration file.
122
123 This follows the git config syntax.
124
125 Args:
126 name: The key to lookup.
127
128 Returns:
129 None if the value was not defined, or is not a boolean.
130 Otherwise, the number itself.
131 """
132 v = self.GetString(name)
133 if v is None:
134 return None
135 v = v.strip()
136
137 mult = 1
138 if v.endswith('k'):
139 v = v[:-1]
140 mult = 1024
141 elif v.endswith('m'):
142 v = v[:-1]
143 mult = 1024 * 1024
144 elif v.endswith('g'):
145 v = v[:-1]
146 mult = 1024 * 1024 * 1024
147
148 base = 10
149 if v.startswith('0x'):
150 base = 16
151
152 try:
153 return int(v, base=base) * mult
154 except ValueError:
155 return None
156
157 def DumpConfigDict(self):
158 """Returns the current configuration dict.
159
160 Configuration data is information only (e.g. logging) and
161 should not be considered a stable data-source.
162
163 Returns:
164 dict of {<key>, <value>} for git configuration cache.
165 <value> are strings converted by GetString.
166 """
167 config_dict = {}
168 for key in self._cache:
169 config_dict[key] = self.GetString(key)
170 return config_dict
171
119 def GetBoolean(self, name): 172 def GetBoolean(self, name):
120 """Returns a boolean from the configuration file. 173 """Returns a boolean from the configuration file.
121 None : The value was not defined, or is not a boolean. 174 None : The value was not defined, or is not a boolean.
@@ -132,6 +185,12 @@ class GitConfig(object):
132 return False 185 return False
133 return None 186 return None
134 187
188 def SetBoolean(self, name, value):
189 """Set the truthy value for a key."""
190 if value is not None:
191 value = 'true' if value else 'false'
192 self.SetString(name, value)
193
135 def GetString(self, name, all_keys=False): 194 def GetString(self, name, all_keys=False):
136 """Get the first value for a key, or None if it is not defined. 195 """Get the first value for a key, or None if it is not defined.
137 196
@@ -142,7 +201,7 @@ class GitConfig(object):
142 v = self._cache[_key(name)] 201 v = self._cache[_key(name)]
143 except KeyError: 202 except KeyError:
144 if self.defaults: 203 if self.defaults:
145 return self.defaults.GetString(name, all_keys = all_keys) 204 return self.defaults.GetString(name, all_keys=all_keys)
146 v = [] 205 v = []
147 206
148 if not all_keys: 207 if not all_keys:
@@ -153,7 +212,7 @@ class GitConfig(object):
153 r = [] 212 r = []
154 r.extend(v) 213 r.extend(v)
155 if self.defaults: 214 if self.defaults:
156 r.extend(self.defaults.GetString(name, all_keys = True)) 215 r.extend(self.defaults.GetString(name, all_keys=True))
157 return r 216 return r
158 217
159 def SetString(self, name, value): 218 def SetString(self, name, value):
@@ -212,12 +271,28 @@ class GitConfig(object):
212 self._branches[b.name] = b 271 self._branches[b.name] = b
213 return b 272 return b
214 273
274 def GetSyncAnalysisStateData(self):
275 """Returns data to be logged for the analysis of sync performance."""
276 return {k: v for k, v in self.DumpConfigDict().items() if k.startswith(SYNC_STATE_PREFIX)}
277
278 def UpdateSyncAnalysisState(self, options, superproject_logging_data):
279 """Update Config's SYNC_STATE_PREFIX* data with the latest sync data.
280
281 Args:
282 options: Options passed to sync returned from optparse. See _Options().
283 superproject_logging_data: A dictionary of superproject data that is to be logged.
284
285 Returns:
286 SyncAnalysisState object.
287 """
288 return SyncAnalysisState(self, options, superproject_logging_data)
289
215 def GetSubSections(self, section): 290 def GetSubSections(self, section):
216 """List all subsection names matching $section.*.* 291 """List all subsection names matching $section.*.*
217 """ 292 """
218 return self._sections.get(section, set()) 293 return self._sections.get(section, set())
219 294
220 def HasSection(self, section, subsection = ''): 295 def HasSection(self, section, subsection=''):
221 """Does at least one key in section.subsection exist? 296 """Does at least one key in section.subsection exist?
222 """ 297 """
223 try: 298 try:
@@ -268,8 +343,7 @@ class GitConfig(object):
268 343
269 def _ReadJson(self): 344 def _ReadJson(self):
270 try: 345 try:
271 if os.path.getmtime(self._json) \ 346 if os.path.getmtime(self._json) <= os.path.getmtime(self.file):
272 <= os.path.getmtime(self.file):
273 platform_utils.remove(self._json) 347 platform_utils.remove(self._json)
274 return None 348 return None
275 except OSError: 349 except OSError:
@@ -278,8 +352,8 @@ class GitConfig(object):
278 Trace(': parsing %s', self.file) 352 Trace(': parsing %s', self.file)
279 with open(self._json) as fd: 353 with open(self._json) as fd:
280 return json.load(fd) 354 return json.load(fd)
281 except (IOError, ValueError): 355 except (IOError, ValueErrorl):
282 platform_utils.remove(self._json) 356 platform_utils.remove(self._json, missing_ok=True)
283 return None 357 return None
284 358
285 def _SaveJson(self, cache): 359 def _SaveJson(self, cache):
@@ -287,8 +361,7 @@ class GitConfig(object):
287 with open(self._json, 'w') as fd: 361 with open(self._json, 'w') as fd:
288 json.dump(cache, fd, indent=2) 362 json.dump(cache, fd, indent=2)
289 except (IOError, TypeError): 363 except (IOError, TypeError):
290 if os.path.exists(self._json): 364 platform_utils.remove(self._json, missing_ok=True)
291 platform_utils.remove(self._json)
292 365
293 def _ReadGit(self): 366 def _ReadGit(self):
294 """ 367 """
@@ -298,11 +371,10 @@ class GitConfig(object):
298 371
299 """ 372 """
300 c = {} 373 c = {}
301 d = self._do('--null', '--list') 374 if not os.path.exists(self.file):
302 if d is None:
303 return c 375 return c
304 if not is_python3(): 376
305 d = d.decode('utf-8') 377 d = self._do('--null', '--list')
306 for line in d.rstrip('\0').split('\0'): 378 for line in d.rstrip('\0').split('\0'):
307 if '\n' in line: 379 if '\n' in line:
308 key, val = line.split('\n', 1) 380 key, val = line.split('\n', 1)
@@ -318,17 +390,26 @@ class GitConfig(object):
318 return c 390 return c
319 391
320 def _do(self, *args): 392 def _do(self, *args):
321 command = ['config', '--file', self.file] 393 if self.file == self._SYSTEM_CONFIG:
394 command = ['config', '--system', '--includes']
395 else:
396 command = ['config', '--file', self.file, '--includes']
322 command.extend(args) 397 command.extend(args)
323 398
324 p = GitCommand(None, 399 p = GitCommand(None,
325 command, 400 command,
326 capture_stdout = True, 401 capture_stdout=True,
327 capture_stderr = True) 402 capture_stderr=True)
328 if p.Wait() == 0: 403 if p.Wait() == 0:
329 return p.stdout 404 return p.stdout
330 else: 405 else:
331 GitError('git config %s: %s' % (str(args), p.stderr)) 406 raise GitError('git config %s: %s' % (str(args), p.stderr))
407
408
409class RepoConfig(GitConfig):
410 """User settings for repo itself."""
411
412 _USER_CONFIG = '~/.repoconfig/config'
332 413
333 414
334class RefSpec(object): 415class RefSpec(object):
@@ -387,133 +468,16 @@ class RefSpec(object):
387 return s 468 return s
388 469
389 470
390_master_processes = []
391_master_keys = set()
392_ssh_master = True
393_master_keys_lock = None
394
395def init_ssh():
396 """Should be called once at the start of repo to init ssh master handling.
397
398 At the moment, all we do is to create our lock.
399 """
400 global _master_keys_lock
401 assert _master_keys_lock is None, "Should only call init_ssh once"
402 _master_keys_lock = _threading.Lock()
403
404def _open_ssh(host, port=None):
405 global _ssh_master
406
407 # Acquire the lock. This is needed to prevent opening multiple masters for
408 # the same host when we're running "repo sync -jN" (for N > 1) _and_ the
409 # manifest <remote fetch="ssh://xyz"> specifies a different host from the
410 # one that was passed to repo init.
411 _master_keys_lock.acquire()
412 try:
413
414 # Check to see whether we already think that the master is running; if we
415 # think it's already running, return right away.
416 if port is not None:
417 key = '%s:%s' % (host, port)
418 else:
419 key = host
420
421 if key in _master_keys:
422 return True
423
424 if not _ssh_master \
425 or 'GIT_SSH' in os.environ \
426 or sys.platform in ('win32', 'cygwin'):
427 # failed earlier, or cygwin ssh can't do this
428 #
429 return False
430
431 # We will make two calls to ssh; this is the common part of both calls.
432 command_base = ['ssh',
433 '-o','ControlPath %s' % ssh_sock(),
434 host]
435 if port is not None:
436 command_base[1:1] = ['-p', str(port)]
437
438 # Since the key wasn't in _master_keys, we think that master isn't running.
439 # ...but before actually starting a master, we'll double-check. This can
440 # be important because we can't tell that that 'git@myhost.com' is the same
441 # as 'myhost.com' where "User git" is setup in the user's ~/.ssh/config file.
442 check_command = command_base + ['-O','check']
443 try:
444 Trace(': %s', ' '.join(check_command))
445 check_process = subprocess.Popen(check_command,
446 stdout=subprocess.PIPE,
447 stderr=subprocess.PIPE)
448 check_process.communicate() # read output, but ignore it...
449 isnt_running = check_process.wait()
450
451 if not isnt_running:
452 # Our double-check found that the master _was_ infact running. Add to
453 # the list of keys.
454 _master_keys.add(key)
455 return True
456 except Exception:
457 # Ignore excpetions. We we will fall back to the normal command and print
458 # to the log there.
459 pass
460
461 command = command_base[:1] + \
462 ['-M', '-N'] + \
463 command_base[1:]
464 try:
465 Trace(': %s', ' '.join(command))
466 p = subprocess.Popen(command)
467 except Exception as e:
468 _ssh_master = False
469 print('\nwarn: cannot enable ssh control master for %s:%s\n%s'
470 % (host,port, str(e)), file=sys.stderr)
471 return False
472
473 time.sleep(1)
474 ssh_died = (p.poll() is not None)
475 if ssh_died:
476 return False
477
478 _master_processes.append(p)
479 _master_keys.add(key)
480 return True
481 finally:
482 _master_keys_lock.release()
483
484def close_ssh():
485 global _master_keys_lock
486
487 terminate_ssh_clients()
488
489 for p in _master_processes:
490 try:
491 os.kill(p.pid, SIGTERM)
492 p.wait()
493 except OSError:
494 pass
495 del _master_processes[:]
496 _master_keys.clear()
497
498 d = ssh_sock(create=False)
499 if d:
500 try:
501 platform_utils.rmdir(os.path.dirname(d))
502 except OSError:
503 pass
504
505 # We're done with the lock, so we can delete it.
506 _master_keys_lock = None
507
508URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):')
509URI_ALL = re.compile(r'^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/') 471URI_ALL = re.compile(r'^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/')
510 472
473
511def GetSchemeFromUrl(url): 474def GetSchemeFromUrl(url):
512 m = URI_ALL.match(url) 475 m = URI_ALL.match(url)
513 if m: 476 if m:
514 return m.group(1) 477 return m.group(1)
515 return None 478 return None
516 479
480
517@contextlib.contextmanager 481@contextlib.contextmanager
518def GetUrlCookieFile(url, quiet): 482def GetUrlCookieFile(url, quiet):
519 if url.startswith('persistent-'): 483 if url.startswith('persistent-'):
@@ -554,29 +518,11 @@ def GetUrlCookieFile(url, quiet):
554 cookiefile = os.path.expanduser(cookiefile) 518 cookiefile = os.path.expanduser(cookiefile)
555 yield cookiefile, None 519 yield cookiefile, None
556 520
557def _preconnect(url):
558 m = URI_ALL.match(url)
559 if m:
560 scheme = m.group(1)
561 host = m.group(2)
562 if ':' in host:
563 host, port = host.split(':')
564 else:
565 port = None
566 if scheme in ('ssh', 'git+ssh', 'ssh+git'):
567 return _open_ssh(host, port)
568 return False
569
570 m = URI_SCP.match(url)
571 if m:
572 host = m.group(1)
573 return _open_ssh(host)
574
575 return False
576 521
577class Remote(object): 522class Remote(object):
578 """Configuration options related to a remote. 523 """Configuration options related to a remote.
579 """ 524 """
525
580 def __init__(self, config, name): 526 def __init__(self, config, name):
581 self._config = config 527 self._config = config
582 self.name = name 528 self.name = name
@@ -585,7 +531,7 @@ class Remote(object):
585 self.review = self._Get('review') 531 self.review = self._Get('review')
586 self.projectname = self._Get('projectname') 532 self.projectname = self._Get('projectname')
587 self.fetch = list(map(RefSpec.FromString, 533 self.fetch = list(map(RefSpec.FromString,
588 self._Get('fetch', all_keys=True))) 534 self._Get('fetch', all_keys=True)))
589 self._review_url = None 535 self._review_url = None
590 536
591 def _InsteadOf(self): 537 def _InsteadOf(self):
@@ -599,8 +545,8 @@ class Remote(object):
599 insteadOfList = globCfg.GetString(key, all_keys=True) 545 insteadOfList = globCfg.GetString(key, all_keys=True)
600 546
601 for insteadOf in insteadOfList: 547 for insteadOf in insteadOfList:
602 if self.url.startswith(insteadOf) \ 548 if (self.url.startswith(insteadOf)
603 and len(insteadOf) > len(longest): 549 and len(insteadOf) > len(longest)):
604 longest = insteadOf 550 longest = insteadOf
605 longestUrl = url 551 longestUrl = url
606 552
@@ -609,9 +555,23 @@ class Remote(object):
609 555
610 return self.url.replace(longest, longestUrl, 1) 556 return self.url.replace(longest, longestUrl, 1)
611 557
612 def PreConnectFetch(self): 558 def PreConnectFetch(self, ssh_proxy):
559 """Run any setup for this remote before we connect to it.
560
561 In practice, if the remote is using SSH, we'll attempt to create a new
562 SSH master session to it for reuse across projects.
563
564 Args:
565 ssh_proxy: The SSH settings for managing master sessions.
566
567 Returns:
568 Whether the preconnect phase for this remote was successful.
569 """
570 if not ssh_proxy:
571 return True
572
613 connectionUrl = self._InsteadOf() 573 connectionUrl = self._InsteadOf()
614 return _preconnect(connectionUrl) 574 return ssh_proxy.preconnect(connectionUrl)
615 575
616 def ReviewUrl(self, userEmail, validate_certs): 576 def ReviewUrl(self, userEmail, validate_certs):
617 if self._review_url is None: 577 if self._review_url is None:
@@ -731,12 +691,13 @@ class Remote(object):
731 691
732 def _Get(self, key, all_keys=False): 692 def _Get(self, key, all_keys=False):
733 key = 'remote.%s.%s' % (self.name, key) 693 key = 'remote.%s.%s' % (self.name, key)
734 return self._config.GetString(key, all_keys = all_keys) 694 return self._config.GetString(key, all_keys=all_keys)
735 695
736 696
737class Branch(object): 697class Branch(object):
738 """Configuration options related to a single branch. 698 """Configuration options related to a single branch.
739 """ 699 """
700
740 def __init__(self, config, name): 701 def __init__(self, config, name):
741 self._config = config 702 self._config = config
742 self.name = name 703 self.name = name
@@ -780,4 +741,71 @@ class Branch(object):
780 741
781 def _Get(self, key, all_keys=False): 742 def _Get(self, key, all_keys=False):
782 key = 'branch.%s.%s' % (self.name, key) 743 key = 'branch.%s.%s' % (self.name, key)
783 return self._config.GetString(key, all_keys = all_keys) 744 return self._config.GetString(key, all_keys=all_keys)
745
746
747class SyncAnalysisState:
748 """Configuration options related to logging of sync state for analysis.
749
750 This object is versioned.
751 """
752 def __init__(self, config, options, superproject_logging_data):
753 """Initializes SyncAnalysisState.
754
755 Saves the following data into the |config| object.
756 - sys.argv, options, superproject's logging data.
757 - repo.*, branch.* and remote.* parameters from config object.
758 - Current time as synctime.
759 - Version number of the object.
760
761 All the keys saved by this object are prepended with SYNC_STATE_PREFIX.
762
763 Args:
764 config: GitConfig object to store all options.
765 options: Options passed to sync returned from optparse. See _Options().
766 superproject_logging_data: A dictionary of superproject data that is to be logged.
767 """
768 self._config = config
769 now = datetime.datetime.utcnow()
770 self._Set('main.synctime', now.isoformat() + 'Z')
771 self._Set('main.version', '1')
772 self._Set('sys.argv', sys.argv)
773 for key, value in superproject_logging_data.items():
774 self._Set(f'superproject.{key}', value)
775 for key, value in options.__dict__.items():
776 self._Set(f'options.{key}', value)
777 config_items = config.DumpConfigDict().items()
778 EXTRACT_NAMESPACES = {'repo', 'branch', 'remote'}
779 self._SetDictionary({k: v for k, v in config_items
780 if not k.startswith(SYNC_STATE_PREFIX) and
781 k.split('.', 1)[0] in EXTRACT_NAMESPACES})
782
783 def _SetDictionary(self, data):
784 """Save all key/value pairs of |data| dictionary.
785
786 Args:
787 data: A dictionary whose key/value are to be saved.
788 """
789 for key, value in data.items():
790 self._Set(key, value)
791
792 def _Set(self, key, value):
793 """Set the |value| for a |key| in the |_config| member.
794
795 |key| is prepended with the value of SYNC_STATE_PREFIX constant.
796
797 Args:
798 key: Name of the key.
799 value: |value| could be of any type. If it is 'bool', it will be saved
800 as a Boolean and for all other types, it will be saved as a String.
801 """
802 if value is None:
803 return
804 sync_key = f'{SYNC_STATE_PREFIX}{key}'
805 sync_key = sync_key.replace('_', '')
806 if isinstance(value, str):
807 self._config.SetString(sync_key, value)
808 elif isinstance(value, bool):
809 self._config.SetBoolean(sync_key, value)
810 else:
811 self._config.SetString(sync_key, str(value))
diff --git a/git_refs.py b/git_refs.py
index debd4cbf..2d4a8090 100644
--- a/git_refs.py
+++ b/git_refs.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2009 The Android Open Source Project 1# Copyright (C) 2009 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,12 +16,14 @@ import os
18from repo_trace import Trace 16from repo_trace import Trace
19import platform_utils 17import platform_utils
20 18
21HEAD = 'HEAD' 19HEAD = 'HEAD'
22R_CHANGES = 'refs/changes/' 20R_CHANGES = 'refs/changes/'
23R_HEADS = 'refs/heads/' 21R_HEADS = 'refs/heads/'
24R_TAGS = 'refs/tags/' 22R_TAGS = 'refs/tags/'
25R_PUB = 'refs/published/' 23R_PUB = 'refs/published/'
26R_M = 'refs/remotes/m/' 24R_WORKTREE = 'refs/worktree/'
25R_WORKTREE_M = R_WORKTREE + 'm/'
26R_M = 'refs/remotes/m/'
27 27
28 28
29class GitRefs(object): 29class GitRefs(object):
@@ -131,11 +131,14 @@ class GitRefs(object):
131 base = os.path.join(self._gitdir, prefix) 131 base = os.path.join(self._gitdir, prefix)
132 for name in platform_utils.listdir(base): 132 for name in platform_utils.listdir(base):
133 p = os.path.join(base, name) 133 p = os.path.join(base, name)
134 if platform_utils.isdir(p): 134 # We don't implement the full ref validation algorithm, just the simple
135 # 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):
135 self._mtime[prefix] = os.path.getmtime(base) 140 self._mtime[prefix] = os.path.getmtime(base)
136 self._ReadLoose(prefix + name + '/') 141 self._ReadLoose(prefix + name + '/')
137 elif name.endswith('.lock'):
138 pass
139 else: 142 else:
140 self._ReadLoose1(p, prefix + name) 143 self._ReadLoose1(p, prefix + name)
141 144
@@ -144,7 +147,7 @@ class GitRefs(object):
144 with open(path) as fd: 147 with open(path) as fd:
145 mtime = os.path.getmtime(path) 148 mtime = os.path.getmtime(path)
146 ref_id = fd.readline() 149 ref_id = fd.readline()
147 except (IOError, OSError): 150 except (OSError, UnicodeError):
148 return 151 return
149 152
150 try: 153 try:
diff --git a/git_superproject.py b/git_superproject.py
new file mode 100644
index 00000000..4ca84a58
--- /dev/null
+++ b/git_superproject.py
@@ -0,0 +1,415 @@
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"""Provide functionality to get all projects and their commit ids from Superproject.
16
17For more information on superproject, check out:
18https://en.wikibooks.org/wiki/Git/Submodules_and_Superprojects
19
20Examples:
21 superproject = Superproject()
22 UpdateProjectsResult = superproject.UpdateProjectsRevisionId(projects)
23"""
24
25import hashlib
26import functools
27import os
28import sys
29import time
30from typing import NamedTuple
31
32from git_command import git_require, GitCommand
33from git_config import RepoConfig
34from git_refs import R_HEADS
35from manifest_xml import LOCAL_MANIFEST_GROUP_PREFIX
36
37_SUPERPROJECT_GIT_NAME = 'superproject.git'
38_SUPERPROJECT_MANIFEST_NAME = 'superproject_override.xml'
39
40
41class SyncResult(NamedTuple):
42 """Return the status of sync and whether caller should exit."""
43
44 # Whether the superproject sync was successful.
45 success: bool
46 # Whether the caller should exit.
47 fatal: bool
48
49
50class CommitIdsResult(NamedTuple):
51 """Return the commit ids and whether caller should exit."""
52
53 # A dictionary with the projects/commit ids on success, otherwise None.
54 commit_ids: dict
55 # Whether the caller should exit.
56 fatal: bool
57
58
59class UpdateProjectsResult(NamedTuple):
60 """Return the overriding manifest file and whether caller should exit."""
61
62 # Path name of the overriding manifest file if successful, otherwise None.
63 manifest_path: str
64 # Whether the caller should exit.
65 fatal: bool
66
67
68class Superproject(object):
69 """Get commit ids from superproject.
70
71 Initializes a local copy of a superproject for the manifest. This allows
72 lookup of commit ids for all projects. It contains _project_commit_ids which
73 is a dictionary with project/commit id entries.
74 """
75 def __init__(self, manifest, repodir, git_event_log,
76 superproject_dir='exp-superproject', quiet=False, print_messages=False):
77 """Initializes superproject.
78
79 Args:
80 manifest: A Manifest object that is to be written to a file.
81 repodir: Path to the .repo/ dir for holding all internal checkout state.
82 It must be in the top directory of the repo client checkout.
83 git_event_log: A git trace2 event log to log events.
84 superproject_dir: Relative path under |repodir| to checkout superproject.
85 quiet: If True then only print the progress messages.
86 print_messages: if True then print error/warning messages.
87 """
88 self._project_commit_ids = None
89 self._manifest = manifest
90 self._git_event_log = git_event_log
91 self._quiet = quiet
92 self._print_messages = print_messages
93 self._branch = manifest.branch
94 self._repodir = os.path.abspath(repodir)
95 self._superproject_dir = superproject_dir
96 self._superproject_path = os.path.join(self._repodir, superproject_dir)
97 self._manifest_path = os.path.join(self._superproject_path,
98 _SUPERPROJECT_MANIFEST_NAME)
99 git_name = ''
100 if self._manifest.superproject:
101 remote = self._manifest.superproject['remote']
102 git_name = hashlib.md5(remote.name.encode('utf8')).hexdigest() + '-'
103 self._branch = self._manifest.superproject['revision']
104 self._remote_url = remote.url
105 else:
106 self._remote_url = None
107 self._work_git_name = git_name + _SUPERPROJECT_GIT_NAME
108 self._work_git = os.path.join(self._superproject_path, self._work_git_name)
109
110 @property
111 def project_commit_ids(self):
112 """Returns a dictionary of projects and their commit ids."""
113 return self._project_commit_ids
114
115 @property
116 def manifest_path(self):
117 """Returns the manifest path if the path exists or None."""
118 return self._manifest_path if os.path.exists(self._manifest_path) else None
119
120 def _LogMessage(self, message):
121 """Logs message to stderr and _git_event_log."""
122 if self._print_messages:
123 print(message, file=sys.stderr)
124 self._git_event_log.ErrorEvent(message, f'{message}')
125
126 def _LogMessagePrefix(self):
127 """Returns the prefix string to be logged in each log message"""
128 return f'repo superproject branch: {self._branch} url: {self._remote_url}'
129
130 def _LogError(self, message):
131 """Logs error message to stderr and _git_event_log."""
132 self._LogMessage(f'{self._LogMessagePrefix()} error: {message}')
133
134 def _LogWarning(self, message):
135 """Logs warning message to stderr and _git_event_log."""
136 self._LogMessage(f'{self._LogMessagePrefix()} warning: {message}')
137
138 def _Init(self):
139 """Sets up a local Git repository to get a copy of a superproject.
140
141 Returns:
142 True if initialization is successful, or False.
143 """
144 if not os.path.exists(self._superproject_path):
145 os.mkdir(self._superproject_path)
146 if not self._quiet and not os.path.exists(self._work_git):
147 print('%s: Performing initial setup for superproject; this might take '
148 'several minutes.' % self._work_git)
149 cmd = ['init', '--bare', self._work_git_name]
150 p = GitCommand(None,
151 cmd,
152 cwd=self._superproject_path,
153 capture_stdout=True,
154 capture_stderr=True)
155 retval = p.Wait()
156 if retval:
157 self._LogWarning(f'git init call failed, command: git {cmd}, '
158 f'return code: {retval}, stderr: {p.stderr}')
159 return False
160 return True
161
162 def _Fetch(self):
163 """Fetches a local copy of a superproject for the manifest based on |_remote_url|.
164
165 Returns:
166 True if fetch is successful, or False.
167 """
168 if not os.path.exists(self._work_git):
169 self._LogWarning(f'git fetch missing directory: {self._work_git}')
170 return False
171 if not git_require((2, 28, 0)):
172 self._LogWarning('superproject requires a git version 2.28 or later')
173 return False
174 cmd = ['fetch', self._remote_url, '--depth', '1', '--force', '--no-tags',
175 '--filter', 'blob:none']
176 if self._branch:
177 cmd += [self._branch + ':' + self._branch]
178 p = GitCommand(None,
179 cmd,
180 cwd=self._work_git,
181 capture_stdout=True,
182 capture_stderr=True)
183 retval = p.Wait()
184 if retval:
185 self._LogWarning(f'git fetch call failed, command: git {cmd}, '
186 f'return code: {retval}, stderr: {p.stderr}')
187 return False
188 return True
189
190 def _LsTree(self):
191 """Gets the commit ids for all projects.
192
193 Works only in git repositories.
194
195 Returns:
196 data: data returned from 'git ls-tree ...' instead of None.
197 """
198 if not os.path.exists(self._work_git):
199 self._LogWarning(f'git ls-tree missing directory: {self._work_git}')
200 return None
201 data = None
202 branch = 'HEAD' if not self._branch else self._branch
203 cmd = ['ls-tree', '-z', '-r', branch]
204
205 p = GitCommand(None,
206 cmd,
207 cwd=self._work_git,
208 capture_stdout=True,
209 capture_stderr=True)
210 retval = p.Wait()
211 if retval == 0:
212 data = p.stdout
213 else:
214 self._LogWarning(f'git ls-tree call failed, command: git {cmd}, '
215 f'return code: {retval}, stderr: {p.stderr}')
216 return data
217
218 def Sync(self):
219 """Gets a local copy of a superproject for the manifest.
220
221 Returns:
222 SyncResult
223 """
224 if not self._manifest.superproject:
225 self._LogWarning(f'superproject tag is not defined in manifest: '
226 f'{self._manifest.manifestFile}')
227 return SyncResult(False, False)
228
229 print('NOTICE: --use-superproject is in beta; report any issues to the '
230 'address described in `repo version`', file=sys.stderr)
231 should_exit = True
232 if not self._remote_url:
233 self._LogWarning(f'superproject URL is not defined in manifest: '
234 f'{self._manifest.manifestFile}')
235 return SyncResult(False, should_exit)
236
237 if not self._Init():
238 return SyncResult(False, should_exit)
239 if not self._Fetch():
240 return SyncResult(False, should_exit)
241 if not self._quiet:
242 print('%s: Initial setup for superproject completed.' % self._work_git)
243 return SyncResult(True, False)
244
245 def _GetAllProjectsCommitIds(self):
246 """Get commit ids for all projects from superproject and save them in _project_commit_ids.
247
248 Returns:
249 CommitIdsResult
250 """
251 sync_result = self.Sync()
252 if not sync_result.success:
253 return CommitIdsResult(None, sync_result.fatal)
254
255 data = self._LsTree()
256 if not data:
257 self._LogWarning(f'git ls-tree failed to return data for manifest: '
258 f'{self._manifest.manifestFile}')
259 return CommitIdsResult(None, True)
260
261 # Parse lines like the following to select lines starting with '160000' and
262 # build a dictionary with project path (last element) and its commit id (3rd element).
263 #
264 # 160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00
265 # 120000 blob acc2cbdf438f9d2141f0ae424cec1d8fc4b5d97f\tbootstrap.bash\x00
266 commit_ids = {}
267 for line in data.split('\x00'):
268 ls_data = line.split(None, 3)
269 if not ls_data:
270 break
271 if ls_data[0] == '160000':
272 commit_ids[ls_data[3]] = ls_data[2]
273
274 self._project_commit_ids = commit_ids
275 return CommitIdsResult(commit_ids, False)
276
277 def _WriteManifestFile(self):
278 """Writes manifest to a file.
279
280 Returns:
281 manifest_path: Path name of the file into which manifest is written instead of None.
282 """
283 if not os.path.exists(self._superproject_path):
284 self._LogWarning(f'missing superproject directory: {self._superproject_path}')
285 return None
286 manifest_str = self._manifest.ToXml(groups=self._manifest.GetGroupsStr()).toxml()
287 manifest_path = self._manifest_path
288 try:
289 with open(manifest_path, 'w', encoding='utf-8') as fp:
290 fp.write(manifest_str)
291 except IOError as e:
292 self._LogError(f'cannot write manifest to : {manifest_path} {e}')
293 return None
294 return manifest_path
295
296 def _SkipUpdatingProjectRevisionId(self, project):
297 """Checks if a project's revision id needs to be updated or not.
298
299 Revision id for projects from local manifest will not be updated.
300
301 Args:
302 project: project whose revision id is being updated.
303
304 Returns:
305 True if a project's revision id should not be updated, or False,
306 """
307 path = project.relpath
308 if not path:
309 return True
310 # Skip the project with revisionId.
311 if project.revisionId:
312 return True
313 # Skip the project if it comes from the local manifest.
314 return any(s.startswith(LOCAL_MANIFEST_GROUP_PREFIX) for s in project.groups)
315
316 def UpdateProjectsRevisionId(self, projects):
317 """Update revisionId of every project in projects with the commit id.
318
319 Args:
320 projects: List of projects whose revisionId needs to be updated.
321
322 Returns:
323 UpdateProjectsResult
324 """
325 commit_ids_result = self._GetAllProjectsCommitIds()
326 commit_ids = commit_ids_result.commit_ids
327 if not commit_ids:
328 return UpdateProjectsResult(None, commit_ids_result.fatal)
329
330 projects_missing_commit_ids = []
331 for project in projects:
332 if self._SkipUpdatingProjectRevisionId(project):
333 continue
334 path = project.relpath
335 commit_id = commit_ids.get(path)
336 if not commit_id:
337 projects_missing_commit_ids.append(path)
338
339 # If superproject doesn't have a commit id for a project, then report an
340 # error event and continue as if do not use superproject is specified.
341 if projects_missing_commit_ids:
342 self._LogWarning(f'please file a bug using {self._manifest.contactinfo.bugurl} '
343 f'to report missing commit_ids for: {projects_missing_commit_ids}')
344 return UpdateProjectsResult(None, False)
345
346 for project in projects:
347 if not self._SkipUpdatingProjectRevisionId(project):
348 project.SetRevisionId(commit_ids.get(project.relpath))
349
350 manifest_path = self._WriteManifestFile()
351 return UpdateProjectsResult(manifest_path, False)
352
353
354@functools.lru_cache(maxsize=None)
355def _UseSuperprojectFromConfiguration():
356 """Returns the user choice of whether to use superproject."""
357 user_cfg = RepoConfig.ForUser()
358 time_now = int(time.time())
359
360 user_value = user_cfg.GetBoolean('repo.superprojectChoice')
361 if user_value is not None:
362 user_expiration = user_cfg.GetInt('repo.superprojectChoiceExpire')
363 if user_expiration is None or user_expiration <= 0 or user_expiration >= time_now:
364 # TODO(b/190688390) - Remove prompt when we are comfortable with the new
365 # default value.
366 if user_value:
367 print(('You are currently enrolled in Git submodules experiment '
368 '(go/android-submodules-quickstart). Use --no-use-superproject '
369 'to override.\n'), file=sys.stderr)
370 else:
371 print(('You are not currently enrolled in Git submodules experiment '
372 '(go/android-submodules-quickstart). Use --use-superproject '
373 'to override.\n'), file=sys.stderr)
374 return user_value
375
376 # We don't have an unexpired choice, ask for one.
377 system_cfg = RepoConfig.ForSystem()
378 system_value = system_cfg.GetBoolean('repo.superprojectChoice')
379 if system_value:
380 # The system configuration is proposing that we should enable the
381 # use of superproject. Treat the user as enrolled for two weeks.
382 #
383 # TODO(b/190688390) - Remove prompt when we are comfortable with the new
384 # default value.
385 userchoice = True
386 time_choiceexpire = time_now + (86400 * 14)
387 user_cfg.SetString('repo.superprojectChoiceExpire', str(time_choiceexpire))
388 user_cfg.SetBoolean('repo.superprojectChoice', userchoice)
389 print('You are automatically enrolled in Git submodules experiment '
390 '(go/android-submodules-quickstart) for another two weeks.\n',
391 file=sys.stderr)
392 return True
393
394 # For all other cases, we would not use superproject by default.
395 return False
396
397
398def PrintMessages(opt, manifest):
399 """Returns a boolean if error/warning messages are to be printed."""
400 return opt.use_superproject is not None or manifest.superproject
401
402
403def UseSuperproject(opt, manifest):
404 """Returns a boolean if use-superproject option is enabled."""
405
406 if opt.use_superproject is not None:
407 return opt.use_superproject
408 else:
409 client_value = manifest.manifestProject.config.GetBoolean('repo.superproject')
410 if client_value is not None:
411 return client_value
412 else:
413 if not manifest.superproject:
414 return False
415 return _UseSuperprojectFromConfiguration()
diff --git a/git_trace2_event_log.py b/git_trace2_event_log.py
new file mode 100644
index 00000000..0e5e9089
--- /dev/null
+++ b/git_trace2_event_log.py
@@ -0,0 +1,273 @@
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"""Provide event logging in the git trace2 EVENT format.
16
17The git trace2 EVENT format is defined at:
18https://www.kernel.org/pub/software/scm/git/docs/technical/api-trace2.html#_event_format
19https://git-scm.com/docs/api-trace2#_the_event_format_target
20
21 Usage:
22
23 git_trace_log = EventLog()
24 git_trace_log.StartEvent()
25 ...
26 git_trace_log.ExitEvent()
27 git_trace_log.Write()
28"""
29
30
31import datetime
32import json
33import os
34import sys
35import tempfile
36import threading
37
38from git_command import GitCommand, RepoSourceVersion
39
40
41class EventLog(object):
42 """Event log that records events that occurred during a repo invocation.
43
44 Events are written to the log as a consecutive JSON entries, one per line.
45 Entries follow the git trace2 EVENT format.
46
47 Each entry contains the following common keys:
48 - event: The event name
49 - sid: session-id - Unique string to allow process instance to be identified.
50 - thread: The thread name.
51 - time: is the UTC time of the event.
52
53 Valid 'event' names and event specific fields are documented here:
54 https://git-scm.com/docs/api-trace2#_event_format
55 """
56
57 def __init__(self, env=None):
58 """Initializes the event log."""
59 self._log = []
60 # Try to get session-id (sid) from environment (setup in repo launcher).
61 KEY = 'GIT_TRACE2_PARENT_SID'
62 if env is None:
63 env = os.environ
64
65 now = datetime.datetime.utcnow()
66
67 # Save both our sid component and the complete sid.
68 # We use our sid component (self._sid) as the unique filename prefix and
69 # the full sid (self._full_sid) in the log itself.
70 self._sid = 'repo-%s-P%08x' % (now.strftime('%Y%m%dT%H%M%SZ'), os.getpid())
71 parent_sid = env.get(KEY)
72 # Append our sid component to the parent sid (if it exists).
73 if parent_sid is not None:
74 self._full_sid = parent_sid + '/' + self._sid
75 else:
76 self._full_sid = self._sid
77
78 # Set/update the environment variable.
79 # Environment handling across systems is messy.
80 try:
81 env[KEY] = self._full_sid
82 except UnicodeEncodeError:
83 env[KEY] = self._full_sid.encode()
84
85 # Add a version event to front of the log.
86 self._AddVersionEvent()
87
88 @property
89 def full_sid(self):
90 return self._full_sid
91
92 def _AddVersionEvent(self):
93 """Adds a 'version' event at the beginning of current log."""
94 version_event = self._CreateEventDict('version')
95 version_event['evt'] = "2"
96 version_event['exe'] = RepoSourceVersion()
97 self._log.insert(0, version_event)
98
99 def _CreateEventDict(self, event_name):
100 """Returns a dictionary with the common keys/values for git trace2 events.
101
102 Args:
103 event_name: The event name.
104
105 Returns:
106 Dictionary with the common event fields populated.
107 """
108 return {
109 'event': event_name,
110 'sid': self._full_sid,
111 'thread': threading.currentThread().getName(),
112 'time': datetime.datetime.utcnow().isoformat() + 'Z',
113 }
114
115 def StartEvent(self):
116 """Append a 'start' event to the current log."""
117 start_event = self._CreateEventDict('start')
118 start_event['argv'] = sys.argv
119 self._log.append(start_event)
120
121 def ExitEvent(self, result):
122 """Append an 'exit' event to the current log.
123
124 Args:
125 result: Exit code of the event
126 """
127 exit_event = self._CreateEventDict('exit')
128
129 # Consider 'None' success (consistent with event_log result handling).
130 if result is None:
131 result = 0
132 exit_event['code'] = result
133 self._log.append(exit_event)
134
135 def CommandEvent(self, name, subcommands):
136 """Append a 'command' event to the current log.
137
138 Args:
139 name: Name of the primary command (ex: repo, git)
140 subcommands: List of the sub-commands (ex: version, init, sync)
141 """
142 command_event = self._CreateEventDict('command')
143 command_event['name'] = name
144 command_event['subcommands'] = subcommands
145 self._log.append(command_event)
146
147 def LogConfigEvents(self, config, event_dict_name):
148 """Append a |event_dict_name| event for each config key in |config|.
149
150 Args:
151 config: Configuration dictionary.
152 event_dict_name: Name of the event dictionary for items to be logged under.
153 """
154 for param, value in config.items():
155 event = self._CreateEventDict(event_dict_name)
156 event['param'] = param
157 event['value'] = value
158 self._log.append(event)
159
160 def DefParamRepoEvents(self, config):
161 """Append a 'def_param' event for each repo.* config key to the current log.
162
163 Args:
164 config: Repo configuration dictionary
165 """
166 # Only output the repo.* config parameters.
167 repo_config = {k: v for k, v in config.items() if k.startswith('repo.')}
168 self.LogConfigEvents(repo_config, 'def_param')
169
170 def GetDataEventName(self, value):
171 """Returns 'data-json' if the value is an array else returns 'data'."""
172 return 'data-json' if value[0] == '[' and value[-1] == ']' else 'data'
173
174 def LogDataConfigEvents(self, config, prefix):
175 """Append a 'data' event for each config key/value in |config| to the current log.
176
177 For each keyX and valueX of the config, "key" field of the event is '|prefix|/keyX'
178 and the "value" of the "key" field is valueX.
179
180 Args:
181 config: Configuration dictionary.
182 prefix: Prefix for each key that is logged.
183 """
184 for key, value in config.items():
185 event = self._CreateEventDict(self.GetDataEventName(value))
186 event['key'] = f'{prefix}/{key}'
187 event['value'] = value
188 self._log.append(event)
189
190 def ErrorEvent(self, msg, fmt):
191 """Append a 'error' event to the current log."""
192 error_event = self._CreateEventDict('error')
193 error_event['msg'] = msg
194 error_event['fmt'] = fmt
195 self._log.append(error_event)
196
197 def _GetEventTargetPath(self):
198 """Get the 'trace2.eventtarget' path from git configuration.
199
200 Returns:
201 path: git config's 'trace2.eventtarget' path if it exists, or None
202 """
203 path = None
204 cmd = ['config', '--get', 'trace2.eventtarget']
205 # TODO(https://crbug.com/gerrit/13706): Use GitConfig when it supports
206 # system git config variables.
207 p = GitCommand(None, cmd, capture_stdout=True, capture_stderr=True,
208 bare=True)
209 retval = p.Wait()
210 if retval == 0:
211 # Strip trailing carriage-return in path.
212 path = p.stdout.rstrip('\n')
213 elif retval != 1:
214 # `git config --get` is documented to produce an exit status of `1` if
215 # the requested variable is not present in the configuration. Report any
216 # other return value as an error.
217 print("repo: error: 'git config --get' call failed with return code: %r, stderr: %r" % (
218 retval, p.stderr), file=sys.stderr)
219 return path
220
221 def Write(self, path=None):
222 """Writes the log out to a file.
223
224 Log is only written if 'path' or 'git config --get trace2.eventtarget'
225 provide a valid path to write logs to.
226
227 Logging filename format follows the git trace2 style of being a unique
228 (exclusive writable) file.
229
230 Args:
231 path: Path to where logs should be written.
232
233 Returns:
234 log_path: Path to the log file if log is written, otherwise None
235 """
236 log_path = None
237 # If no logging path is specified, get the path from 'trace2.eventtarget'.
238 if path is None:
239 path = self._GetEventTargetPath()
240
241 # If no logging path is specified, exit.
242 if path is None:
243 return None
244
245 if isinstance(path, str):
246 # Get absolute path.
247 path = os.path.abspath(os.path.expanduser(path))
248 else:
249 raise TypeError('path: str required but got %s.' % type(path))
250
251 # Git trace2 requires a directory to write log to.
252
253 # TODO(https://crbug.com/gerrit/13706): Support file (append) mode also.
254 if not os.path.isdir(path):
255 return None
256 # Use NamedTemporaryFile to generate a unique filename as required by git trace2.
257 try:
258 with tempfile.NamedTemporaryFile(mode='x', prefix=self._sid, dir=path,
259 delete=False) as f:
260 # TODO(https://crbug.com/gerrit/13706): Support writing events as they
261 # occur.
262 for e in self._log:
263 # Dump in compact encoding mode.
264 # See 'Compact encoding' in Python docs:
265 # https://docs.python.org/3/library/json.html#module-json
266 json.dump(e, f, indent=None, separators=(',', ':'))
267 f.write('\n')
268 log_path = f.name
269 except FileExistsError as err:
270 print('repo: warning: git trace2 logging failed: %r' % err,
271 file=sys.stderr)
272 return None
273 return log_path
diff --git a/gitc_utils.py b/gitc_utils.py
index b47e181c..486bbeb0 100644
--- a/gitc_utils.py
+++ b/gitc_utils.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2015 The Android Open Source Project 1# Copyright (C) 2015 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,8 +12,8 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function
18import os 15import os
16import multiprocessing
19import platform 17import platform
20import re 18import re
21import sys 19import sys
@@ -29,12 +27,24 @@ from error import ManifestParseError
29 27
30NUM_BATCH_RETRIEVE_REVISIONID = 32 28NUM_BATCH_RETRIEVE_REVISIONID = 32
31 29
30
32def get_gitc_manifest_dir(): 31def get_gitc_manifest_dir():
33 return wrapper.Wrapper().get_gitc_manifest_dir() 32 return wrapper.Wrapper().get_gitc_manifest_dir()
34 33
34
35def parse_clientdir(gitc_fs_path): 35def parse_clientdir(gitc_fs_path):
36 return wrapper.Wrapper().gitc_parse_clientdir(gitc_fs_path) 36 return wrapper.Wrapper().gitc_parse_clientdir(gitc_fs_path)
37 37
38
39def _get_project_revision(args):
40 """Worker for _set_project_revisions to lookup one project remote."""
41 (i, url, expr) = args
42 gitcmd = git_command.GitCommand(
43 None, ['ls-remote', url, expr], capture_stdout=True, cwd='/tmp')
44 rc = gitcmd.Wait()
45 return (i, rc, gitcmd.stdout.split('\t', 1)[0])
46
47
38def _set_project_revisions(projects): 48def _set_project_revisions(projects):
39 """Sets the revisionExpr for a list of projects. 49 """Sets the revisionExpr for a list of projects.
40 50
@@ -42,47 +52,38 @@ def _set_project_revisions(projects):
42 should not be overly large. Recommend calling this function multiple times 52 should not be overly large. Recommend calling this function multiple times
43 with each call not exceeding NUM_BATCH_RETRIEVE_REVISIONID projects. 53 with each call not exceeding NUM_BATCH_RETRIEVE_REVISIONID projects.
44 54
45 @param projects: List of project objects to set the revionExpr for. 55 Args:
56 projects: List of project objects to set the revionExpr for.
46 """ 57 """
47 # Retrieve the commit id for each project based off of it's current 58 # Retrieve the commit id for each project based off of it's current
48 # revisionExpr and it is not already a commit id. 59 # revisionExpr and it is not already a commit id.
49 project_gitcmds = [( 60 with multiprocessing.Pool(NUM_BATCH_RETRIEVE_REVISIONID) as pool:
50 project, git_command.GitCommand(None, 61 results_iter = pool.imap_unordered(
51 ['ls-remote', 62 _get_project_revision,
52 project.remote.url, 63 ((i, project.remote.url, project.revisionExpr)
53 project.revisionExpr], 64 for i, project in enumerate(projects)
54 capture_stdout=True, cwd='/tmp')) 65 if not git_config.IsId(project.revisionExpr)),
55 for project in projects if not git_config.IsId(project.revisionExpr)] 66 chunksize=8)
56 for proj, gitcmd in project_gitcmds: 67 for (i, rc, revisionExpr) in results_iter:
57 if gitcmd.Wait(): 68 project = projects[i]
58 print('FATAL: Failed to retrieve revisionExpr for %s' % proj) 69 if rc:
59 sys.exit(1) 70 print('FATAL: Failed to retrieve revisionExpr for %s' % project.name)
60 revisionExpr = gitcmd.stdout.split('\t')[0] 71 pool.terminate()
61 if not revisionExpr: 72 sys.exit(1)
62 raise ManifestParseError('Invalid SHA-1 revision project %s (%s)' % 73 if not revisionExpr:
63 (proj.remote.url, proj.revisionExpr)) 74 pool.terminate()
64 proj.revisionExpr = revisionExpr 75 raise ManifestParseError('Invalid SHA-1 revision project %s (%s)' %
65 76 (project.remote.url, project.revisionExpr))
66def _manifest_groups(manifest): 77 project.revisionExpr = revisionExpr
67 """Returns the manifest group string that should be synced 78
68
69 This is the same logic used by Command.GetProjects(), which is used during
70 repo sync
71
72 @param manifest: The XmlManifest object
73 """
74 mp = manifest.manifestProject
75 groups = mp.config.GetString('manifest.groups')
76 if not groups:
77 groups = 'default,platform-' + platform.system().lower()
78 return groups
79 79
80def generate_gitc_manifest(gitc_manifest, manifest, paths=None): 80def generate_gitc_manifest(gitc_manifest, manifest, paths=None):
81 """Generate a manifest for shafsd to use for this GITC client. 81 """Generate a manifest for shafsd to use for this GITC client.
82 82
83 @param gitc_manifest: Current gitc manifest, or None if there isn't one yet. 83 Args:
84 @param manifest: A GitcManifest object loaded with the current repo manifest. 84 gitc_manifest: Current gitc manifest, or None if there isn't one yet.
85 @param paths: List of project paths we want to update. 85 manifest: A GitcManifest object loaded with the current repo manifest.
86 paths: List of project paths we want to update.
86 """ 87 """
87 88
88 print('Generating GITC Manifest by fetching revision SHAs for each ' 89 print('Generating GITC Manifest by fetching revision SHAs for each '
@@ -90,7 +91,7 @@ def generate_gitc_manifest(gitc_manifest, manifest, paths=None):
90 if paths is None: 91 if paths is None:
91 paths = list(manifest.paths.keys()) 92 paths = list(manifest.paths.keys())
92 93
93 groups = [x for x in re.split(r'[,\s]+', _manifest_groups(manifest)) if x] 94 groups = [x for x in re.split(r'[,\s]+', manifest.GetGroupsStr()) if x]
94 95
95 # Convert the paths to projects, and filter them to the matched groups. 96 # Convert the paths to projects, and filter them to the matched groups.
96 projects = [manifest.paths[p] for p in paths] 97 projects = [manifest.paths[p] for p in paths]
@@ -104,11 +105,11 @@ def generate_gitc_manifest(gitc_manifest, manifest, paths=None):
104 if not proj.upstream and not git_config.IsId(proj.revisionExpr): 105 if not proj.upstream and not git_config.IsId(proj.revisionExpr):
105 proj.upstream = proj.revisionExpr 106 proj.upstream = proj.revisionExpr
106 107
107 if not path in gitc_manifest.paths: 108 if path not in gitc_manifest.paths:
108 # Any new projects need their first revision, even if we weren't asked 109 # Any new projects need their first revision, even if we weren't asked
109 # for them. 110 # for them.
110 projects.append(proj) 111 projects.append(proj)
111 elif not path in paths: 112 elif path not in paths:
112 # And copy revisions from the previous manifest if we're not updating 113 # And copy revisions from the previous manifest if we're not updating
113 # them now. 114 # them now.
114 gitc_proj = gitc_manifest.paths[path] 115 gitc_proj = gitc_manifest.paths[path]
@@ -118,11 +119,7 @@ def generate_gitc_manifest(gitc_manifest, manifest, paths=None):
118 else: 119 else:
119 proj.revisionExpr = gitc_proj.revisionExpr 120 proj.revisionExpr = gitc_proj.revisionExpr
120 121
121 index = 0 122 _set_project_revisions(projects)
122 while index < len(projects):
123 _set_project_revisions(
124 projects[index:(index+NUM_BATCH_RETRIEVE_REVISIONID)])
125 index += NUM_BATCH_RETRIEVE_REVISIONID
126 123
127 if gitc_manifest is not None: 124 if gitc_manifest is not None:
128 for path, proj in gitc_manifest.paths.items(): 125 for path, proj in gitc_manifest.paths.items():
@@ -140,16 +137,20 @@ def generate_gitc_manifest(gitc_manifest, manifest, paths=None):
140 # Save the manifest. 137 # Save the manifest.
141 save_manifest(manifest) 138 save_manifest(manifest)
142 139
140
143def save_manifest(manifest, client_dir=None): 141def save_manifest(manifest, client_dir=None):
144 """Save the manifest file in the client_dir. 142 """Save the manifest file in the client_dir.
145 143
146 @param client_dir: Client directory to save the manifest in. 144 Args:
147 @param manifest: Manifest object to save. 145 manifest: Manifest object to save.
146 client_dir: Client directory to save the manifest in.
148 """ 147 """
149 if not client_dir: 148 if not client_dir:
150 client_dir = manifest.gitc_client_dir 149 manifest_file = manifest.manifestFile
151 with open(os.path.join(client_dir, '.manifest'), 'w') as f: 150 else:
152 manifest.Save(f, groups=_manifest_groups(manifest)) 151 manifest_file = os.path.join(client_dir, '.manifest')
152 with open(manifest_file, 'w') as f:
153 manifest.Save(f, groups=manifest.GetGroupsStr())
153 # TODO(sbasi/jorg): Come up with a solution to remove the sleep below. 154 # TODO(sbasi/jorg): Come up with a solution to remove the sleep below.
154 # Give the GITC filesystem time to register the manifest changes. 155 # Give the GITC filesystem time to register the manifest changes.
155 time.sleep(3) 156 time.sleep(3)
diff --git a/hooks.py b/hooks.py
new file mode 100644
index 00000000..67c21a25
--- /dev/null
+++ b/hooks.py
@@ -0,0 +1,509 @@
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
15import errno
16import json
17import os
18import re
19import subprocess
20import sys
21import traceback
22import urllib.parse
23
24from error import HookError
25from git_refs import HEAD
26
27
28class RepoHook(object):
29 """A RepoHook contains information about a script to run as a hook.
30
31 Hooks are used to run a python script before running an upload (for instance,
32 to run presubmit checks). Eventually, we may have hooks for other actions.
33
34 This shouldn't be confused with files in the 'repo/hooks' directory. Those
35 files are copied into each '.git/hooks' folder for each project. Repo-level
36 hooks are associated instead with repo actions.
37
38 Hooks are always python. When a hook is run, we will load the hook into the
39 interpreter and execute its main() function.
40
41 Combinations of hook option flags:
42 - no-verify=False, verify=False (DEFAULT):
43 If stdout is a tty, can prompt about running hooks if needed.
44 If user denies running hooks, the action is cancelled. If stdout is
45 not a tty and we would need to prompt about hooks, action is
46 cancelled.
47 - no-verify=False, verify=True:
48 Always run hooks with no prompt.
49 - no-verify=True, verify=False:
50 Never run hooks, but run action anyway (AKA bypass hooks).
51 - no-verify=True, verify=True:
52 Invalid
53 """
54
55 def __init__(self,
56 hook_type,
57 hooks_project,
58 repo_topdir,
59 manifest_url,
60 bypass_hooks=False,
61 allow_all_hooks=False,
62 ignore_hooks=False,
63 abort_if_user_denies=False):
64 """RepoHook constructor.
65
66 Params:
67 hook_type: A string representing the type of hook. This is also used
68 to figure out the name of the file containing the hook. For
69 example: 'pre-upload'.
70 hooks_project: The project containing the repo hooks.
71 If you have a manifest, this is manifest.repo_hooks_project.
72 OK if this is None, which will make the hook a no-op.
73 repo_topdir: The top directory of the repo client checkout.
74 This is the one containing the .repo directory. Scripts will
75 run with CWD as this directory.
76 If you have a manifest, this is manifest.topdir.
77 manifest_url: The URL to the manifest git repo.
78 bypass_hooks: If True, then 'Do not run the hook'.
79 allow_all_hooks: If True, then 'Run the hook without prompting'.
80 ignore_hooks: If True, then 'Do not abort action if hooks fail'.
81 abort_if_user_denies: If True, we'll abort running the hook if the user
82 doesn't allow us to run the hook.
83 """
84 self._hook_type = hook_type
85 self._hooks_project = hooks_project
86 self._repo_topdir = repo_topdir
87 self._manifest_url = manifest_url
88 self._bypass_hooks = bypass_hooks
89 self._allow_all_hooks = allow_all_hooks
90 self._ignore_hooks = ignore_hooks
91 self._abort_if_user_denies = abort_if_user_denies
92
93 # Store the full path to the script for convenience.
94 if self._hooks_project:
95 self._script_fullpath = os.path.join(self._hooks_project.worktree,
96 self._hook_type + '.py')
97 else:
98 self._script_fullpath = None
99
100 def _GetHash(self):
101 """Return a hash of the contents of the hooks directory.
102
103 We'll just use git to do this. This hash has the property that if anything
104 changes in the directory we will return a different has.
105
106 SECURITY CONSIDERATION:
107 This hash only represents the contents of files in the hook directory, not
108 any other files imported or called by hooks. Changes to imported files
109 can change the script behavior without affecting the hash.
110
111 Returns:
112 A string representing the hash. This will always be ASCII so that it can
113 be printed to the user easily.
114 """
115 assert self._hooks_project, "Must have hooks to calculate their hash."
116
117 # We will use the work_git object rather than just calling GetRevisionId().
118 # That gives us a hash of the latest checked in version of the files that
119 # the user will actually be executing. Specifically, GetRevisionId()
120 # doesn't appear to change even if a user checks out a different version
121 # of the hooks repo (via git checkout) nor if a user commits their own revs.
122 #
123 # NOTE: Local (non-committed) changes will not be factored into this hash.
124 # I think this is OK, since we're really only worried about warning the user
125 # about upstream changes.
126 return self._hooks_project.work_git.rev_parse(HEAD)
127
128 def _GetMustVerb(self):
129 """Return 'must' if the hook is required; 'should' if not."""
130 if self._abort_if_user_denies:
131 return 'must'
132 else:
133 return 'should'
134
135 def _CheckForHookApproval(self):
136 """Check to see whether this hook has been approved.
137
138 We'll accept approval of manifest URLs if they're using secure transports.
139 This way the user can say they trust the manifest hoster. For insecure
140 hosts, we fall back to checking the hash of the hooks repo.
141
142 Note that we ask permission for each individual hook even though we use
143 the hash of all hooks when detecting changes. We'd like the user to be
144 able to approve / deny each hook individually. We only use the hash of all
145 hooks because there is no other easy way to detect changes to local imports.
146
147 Returns:
148 True if this hook is approved to run; False otherwise.
149
150 Raises:
151 HookError: Raised if the user doesn't approve and abort_if_user_denies
152 was passed to the consturctor.
153 """
154 if self._ManifestUrlHasSecureScheme():
155 return self._CheckForHookApprovalManifest()
156 else:
157 return self._CheckForHookApprovalHash()
158
159 def _CheckForHookApprovalHelper(self, subkey, new_val, main_prompt,
160 changed_prompt):
161 """Check for approval for a particular attribute and hook.
162
163 Args:
164 subkey: The git config key under [repo.hooks.<hook_type>] to store the
165 last approved string.
166 new_val: The new value to compare against the last approved one.
167 main_prompt: Message to display to the user to ask for approval.
168 changed_prompt: Message explaining why we're re-asking for approval.
169
170 Returns:
171 True if this hook is approved to run; False otherwise.
172
173 Raises:
174 HookError: Raised if the user doesn't approve and abort_if_user_denies
175 was passed to the consturctor.
176 """
177 hooks_config = self._hooks_project.config
178 git_approval_key = 'repo.hooks.%s.%s' % (self._hook_type, subkey)
179
180 # Get the last value that the user approved for this hook; may be None.
181 old_val = hooks_config.GetString(git_approval_key)
182
183 if old_val is not None:
184 # User previously approved hook and asked not to be prompted again.
185 if new_val == old_val:
186 # Approval matched. We're done.
187 return True
188 else:
189 # Give the user a reason why we're prompting, since they last told
190 # us to "never ask again".
191 prompt = 'WARNING: %s\n\n' % (changed_prompt,)
192 else:
193 prompt = ''
194
195 # Prompt the user if we're not on a tty; on a tty we'll assume "no".
196 if sys.stdout.isatty():
197 prompt += main_prompt + ' (yes/always/NO)? '
198 response = input(prompt).lower()
199 print()
200
201 # User is doing a one-time approval.
202 if response in ('y', 'yes'):
203 return True
204 elif response == 'always':
205 hooks_config.SetString(git_approval_key, new_val)
206 return True
207
208 # For anything else, we'll assume no approval.
209 if self._abort_if_user_denies:
210 raise HookError('You must allow the %s hook or use --no-verify.' %
211 self._hook_type)
212
213 return False
214
215 def _ManifestUrlHasSecureScheme(self):
216 """Check if the URI for the manifest is a secure transport."""
217 secure_schemes = ('file', 'https', 'ssh', 'persistent-https', 'sso', 'rpc')
218 parse_results = urllib.parse.urlparse(self._manifest_url)
219 return parse_results.scheme in secure_schemes
220
221 def _CheckForHookApprovalManifest(self):
222 """Check whether the user has approved this manifest host.
223
224 Returns:
225 True if this hook is approved to run; False otherwise.
226 """
227 return self._CheckForHookApprovalHelper(
228 'approvedmanifest',
229 self._manifest_url,
230 'Run hook scripts from %s' % (self._manifest_url,),
231 'Manifest URL has changed since %s was allowed.' % (self._hook_type,))
232
233 def _CheckForHookApprovalHash(self):
234 """Check whether the user has approved the hooks repo.
235
236 Returns:
237 True if this hook is approved to run; False otherwise.
238 """
239 prompt = ('Repo %s run the script:\n'
240 ' %s\n'
241 '\n'
242 'Do you want to allow this script to run')
243 return self._CheckForHookApprovalHelper(
244 'approvedhash',
245 self._GetHash(),
246 prompt % (self._GetMustVerb(), self._script_fullpath),
247 'Scripts have changed since %s was allowed.' % (self._hook_type,))
248
249 @staticmethod
250 def _ExtractInterpFromShebang(data):
251 """Extract the interpreter used in the shebang.
252
253 Try to locate the interpreter the script is using (ignoring `env`).
254
255 Args:
256 data: The file content of the script.
257
258 Returns:
259 The basename of the main script interpreter, or None if a shebang is not
260 used or could not be parsed out.
261 """
262 firstline = data.splitlines()[:1]
263 if not firstline:
264 return None
265
266 # The format here can be tricky.
267 shebang = firstline[0].strip()
268 m = re.match(r'^#!\s*([^\s]+)(?:\s+([^\s]+))?', shebang)
269 if not m:
270 return None
271
272 # If the using `env`, find the target program.
273 interp = m.group(1)
274 if os.path.basename(interp) == 'env':
275 interp = m.group(2)
276
277 return interp
278
279 def _ExecuteHookViaReexec(self, interp, context, **kwargs):
280 """Execute the hook script through |interp|.
281
282 Note: Support for this feature should be dropped ~Jun 2021.
283
284 Args:
285 interp: The Python program to run.
286 context: Basic Python context to execute the hook inside.
287 kwargs: Arbitrary arguments to pass to the hook script.
288
289 Raises:
290 HookError: When the hooks failed for any reason.
291 """
292 # This logic needs to be kept in sync with _ExecuteHookViaImport below.
293 script = """
294import json, os, sys
295path = '''%(path)s'''
296kwargs = json.loads('''%(kwargs)s''')
297context = json.loads('''%(context)s''')
298sys.path.insert(0, os.path.dirname(path))
299data = open(path).read()
300exec(compile(data, path, 'exec'), context)
301context['main'](**kwargs)
302""" % {
303 'path': self._script_fullpath,
304 'kwargs': json.dumps(kwargs),
305 'context': json.dumps(context),
306 }
307
308 # We pass the script via stdin to avoid OS argv limits. It also makes
309 # unhandled exception tracebacks less verbose/confusing for users.
310 cmd = [interp, '-c', 'import sys; exec(sys.stdin.read())']
311 proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
312 proc.communicate(input=script.encode('utf-8'))
313 if proc.returncode:
314 raise HookError('Failed to run %s hook.' % (self._hook_type,))
315
316 def _ExecuteHookViaImport(self, data, context, **kwargs):
317 """Execute the hook code in |data| directly.
318
319 Args:
320 data: The code of the hook to execute.
321 context: Basic Python context to execute the hook inside.
322 kwargs: Arbitrary arguments to pass to the hook script.
323
324 Raises:
325 HookError: When the hooks failed for any reason.
326 """
327 # Exec, storing global context in the context dict. We catch exceptions
328 # and convert to a HookError w/ just the failing traceback.
329 try:
330 exec(compile(data, self._script_fullpath, 'exec'), context)
331 except Exception:
332 raise HookError('%s\nFailed to import %s hook; see traceback above.' %
333 (traceback.format_exc(), self._hook_type))
334
335 # Running the script should have defined a main() function.
336 if 'main' not in context:
337 raise HookError('Missing main() in: "%s"' % self._script_fullpath)
338
339 # Call the main function in the hook. If the hook should cause the
340 # build to fail, it will raise an Exception. We'll catch that convert
341 # to a HookError w/ just the failing traceback.
342 try:
343 context['main'](**kwargs)
344 except Exception:
345 raise HookError('%s\nFailed to run main() for %s hook; see traceback '
346 'above.' % (traceback.format_exc(), self._hook_type))
347
348 def _ExecuteHook(self, **kwargs):
349 """Actually execute the given hook.
350
351 This will run the hook's 'main' function in our python interpreter.
352
353 Args:
354 kwargs: Keyword arguments to pass to the hook. These are often specific
355 to the hook type. For instance, pre-upload hooks will contain
356 a project_list.
357 """
358 # Keep sys.path and CWD stashed away so that we can always restore them
359 # upon function exit.
360 orig_path = os.getcwd()
361 orig_syspath = sys.path
362
363 try:
364 # Always run hooks with CWD as topdir.
365 os.chdir(self._repo_topdir)
366
367 # Put the hook dir as the first item of sys.path so hooks can do
368 # relative imports. We want to replace the repo dir as [0] so
369 # hooks can't import repo files.
370 sys.path = [os.path.dirname(self._script_fullpath)] + sys.path[1:]
371
372 # Initial global context for the hook to run within.
373 context = {'__file__': self._script_fullpath}
374
375 # Add 'hook_should_take_kwargs' to the arguments to be passed to main.
376 # We don't actually want hooks to define their main with this argument--
377 # it's there to remind them that their hook should always take **kwargs.
378 # For instance, a pre-upload hook should be defined like:
379 # def main(project_list, **kwargs):
380 #
381 # This allows us to later expand the API without breaking old hooks.
382 kwargs = kwargs.copy()
383 kwargs['hook_should_take_kwargs'] = True
384
385 # See what version of python the hook has been written against.
386 data = open(self._script_fullpath).read()
387 interp = self._ExtractInterpFromShebang(data)
388 reexec = False
389 if interp:
390 prog = os.path.basename(interp)
391 if prog.startswith('python2') and sys.version_info.major != 2:
392 reexec = True
393 elif prog.startswith('python3') and sys.version_info.major == 2:
394 reexec = True
395
396 # Attempt to execute the hooks through the requested version of Python.
397 if reexec:
398 try:
399 self._ExecuteHookViaReexec(interp, context, **kwargs)
400 except OSError as e:
401 if e.errno == errno.ENOENT:
402 # We couldn't find the interpreter, so fallback to importing.
403 reexec = False
404 else:
405 raise
406
407 # Run the hook by importing directly.
408 if not reexec:
409 self._ExecuteHookViaImport(data, context, **kwargs)
410 finally:
411 # Restore sys.path and CWD.
412 sys.path = orig_syspath
413 os.chdir(orig_path)
414
415 def _CheckHook(self):
416 # Bail with a nice error if we can't find the hook.
417 if not os.path.isfile(self._script_fullpath):
418 raise HookError('Couldn\'t find repo hook: %s' % self._script_fullpath)
419
420 def Run(self, **kwargs):
421 """Run the hook.
422
423 If the hook doesn't exist (because there is no hooks project or because
424 this particular hook is not enabled), this is a no-op.
425
426 Args:
427 user_allows_all_hooks: If True, we will never prompt about running the
428 hook--we'll just assume it's OK to run it.
429 kwargs: Keyword arguments to pass to the hook. These are often specific
430 to the hook type. For instance, pre-upload hooks will contain
431 a project_list.
432
433 Returns:
434 True: On success or ignore hooks by user-request
435 False: The hook failed. The caller should respond with aborting the action.
436 Some examples in which False is returned:
437 * Finding the hook failed while it was enabled, or
438 * the user declined to run a required hook (from _CheckForHookApproval)
439 In all these cases the user did not pass the proper arguments to
440 ignore the result through the option combinations as listed in
441 AddHookOptionGroup().
442 """
443 # Do not do anything in case bypass_hooks is set, or
444 # no-op if there is no hooks project or if hook is disabled.
445 if (self._bypass_hooks or
446 not self._hooks_project or
447 self._hook_type not in self._hooks_project.enabled_repo_hooks):
448 return True
449
450 passed = True
451 try:
452 self._CheckHook()
453
454 # Make sure the user is OK with running the hook.
455 if self._allow_all_hooks or self._CheckForHookApproval():
456 # Run the hook with the same version of python we're using.
457 self._ExecuteHook(**kwargs)
458 except SystemExit as e:
459 passed = False
460 print('ERROR: %s hooks exited with exit code: %s' % (self._hook_type, str(e)),
461 file=sys.stderr)
462 except HookError as e:
463 passed = False
464 print('ERROR: %s' % str(e), file=sys.stderr)
465
466 if not passed and self._ignore_hooks:
467 print('\nWARNING: %s hooks failed, but continuing anyways.' % self._hook_type,
468 file=sys.stderr)
469 passed = True
470
471 return passed
472
473 @classmethod
474 def FromSubcmd(cls, manifest, opt, *args, **kwargs):
475 """Method to construct the repo hook class
476
477 Args:
478 manifest: The current active manifest for this command from which we
479 extract a couple of fields.
480 opt: Contains the commandline options for the action of this hook.
481 It should contain the options added by AddHookOptionGroup() in which
482 we are interested in RepoHook execution.
483 """
484 for key in ('bypass_hooks', 'allow_all_hooks', 'ignore_hooks'):
485 kwargs.setdefault(key, getattr(opt, key))
486 kwargs.update({
487 'hooks_project': manifest.repo_hooks_project,
488 'repo_topdir': manifest.topdir,
489 'manifest_url': manifest.manifestProject.GetRemote('origin').url,
490 })
491 return cls(*args, **kwargs)
492
493 @staticmethod
494 def AddOptionGroup(parser, name):
495 """Help options relating to the various hooks."""
496
497 # Note that verify and no-verify are NOT opposites of each other, which
498 # is why they store to different locations. We are using them to match
499 # 'git commit' syntax.
500 group = parser.add_option_group(name + ' hooks')
501 group.add_option('--no-verify',
502 dest='bypass_hooks', action='store_true',
503 help='Do not run the %s hook.' % name)
504 group.add_option('--verify',
505 dest='allow_all_hooks', action='store_true',
506 help='Run the %s hook without prompting.' % name)
507 group.add_option('--ignore-hooks',
508 action='store_true',
509 help='Do not abort if %s hooks fail.' % name)
diff --git a/hooks/commit-msg b/hooks/commit-msg
index d9bb188b..70d67eaf 100755
--- a/hooks/commit-msg
+++ b/hooks/commit-msg
@@ -1,5 +1,5 @@
1#!/bin/sh 1#!/bin/sh
2# From Gerrit Code Review 2.14.6 2# From Gerrit Code Review 3.1.3
3# 3#
4# Part of Gerrit Code Review (https://www.gerritcodereview.com/) 4# Part of Gerrit Code Review (https://www.gerritcodereview.com/)
5# 5#
@@ -16,176 +16,48 @@
16# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17# See the License for the specific language governing permissions and 17# See the License for the specific language governing permissions and
18# limitations under the License. 18# limitations under the License.
19#
20
21unset GREP_OPTIONS
22
23CHANGE_ID_AFTER="Bug|Depends-On|Issue|Test|Feature|Fixes|Fixed"
24MSG="$1"
25
26# Check for, and add if missing, a unique Change-Id
27#
28add_ChangeId() {
29 clean_message=`sed -e '
30 /^diff --git .*/{
31 s///
32 q
33 }
34 /^Signed-off-by:/d
35 /^#/d
36 ' "$MSG" | git stripspace`
37 if test -z "$clean_message"
38 then
39 return
40 fi
41
42 # Do not add Change-Id to temp commits
43 if echo "$clean_message" | head -1 | grep -q '^\(fixup\|squash\)!'
44 then
45 return
46 fi
47
48 if test "false" = "`git config --bool --get gerrit.createChangeId`"
49 then
50 return
51 fi
52
53 # Does Change-Id: already exist? if so, exit (no change).
54 if grep -i '^Change-Id:' "$MSG" >/dev/null
55 then
56 return
57 fi
58
59 id=`_gen_ChangeId`
60 T="$MSG.tmp.$$"
61 AWK=awk
62 if [ -x /usr/xpg4/bin/awk ]; then
63 # Solaris AWK is just too broken
64 AWK=/usr/xpg4/bin/awk
65 fi
66
67 # Get core.commentChar from git config or use default symbol
68 commentChar=`git config --get core.commentChar`
69 commentChar=${commentChar:-#}
70
71 # How this works:
72 # - parse the commit message as (textLine+ blankLine*)*
73 # - assume textLine+ to be a footer until proven otherwise
74 # - exception: the first block is not footer (as it is the title)
75 # - read textLine+ into a variable
76 # - then count blankLines
77 # - once the next textLine appears, print textLine+ blankLine* as these
78 # aren't footer
79 # - in END, the last textLine+ block is available for footer parsing
80 $AWK '
81 BEGIN {
82 # while we start with the assumption that textLine+
83 # is a footer, the first block is not.
84 isFooter = 0
85 footerComment = 0
86 blankLines = 0
87 }
88
89 # Skip lines starting with commentChar without any spaces before it.
90 /^'"$commentChar"'/ { next }
91
92 # Skip the line starting with the diff command and everything after it,
93 # up to the end of the file, assuming it is only patch data.
94 # If more than one line before the diff was empty, strip all but one.
95 /^diff --git / {
96 blankLines = 0
97 while (getline) { }
98 next
99 }
100
101 # Count blank lines outside footer comments
102 /^$/ && (footerComment == 0) {
103 blankLines++
104 next
105 }
106
107 # Catch footer comment
108 /^\[[a-zA-Z0-9-]+:/ && (isFooter == 1) {
109 footerComment = 1
110 }
111
112 /]$/ && (footerComment == 1) {
113 footerComment = 2
114 }
115
116 # We have a non-blank line after blank lines. Handle this.
117 (blankLines > 0) {
118 print lines
119 for (i = 0; i < blankLines; i++) {
120 print ""
121 }
122
123 lines = ""
124 blankLines = 0
125 isFooter = 1
126 footerComment = 0
127 }
128
129 # Detect that the current block is not the footer
130 (footerComment == 0) && (!/^\[?[a-zA-Z0-9-]+:/ || /^[a-zA-Z0-9-]+:\/\//) {
131 isFooter = 0
132 }
133
134 {
135 # We need this information about the current last comment line
136 if (footerComment == 2) {
137 footerComment = 0
138 }
139 if (lines != "") {
140 lines = lines "\n";
141 }
142 lines = lines $0
143 }
144
145 # Footer handling:
146 # If the last block is considered a footer, splice in the Change-Id at the
147 # right place.
148 # Look for the right place to inject Change-Id by considering
149 # CHANGE_ID_AFTER. Keys listed in it (case insensitive) come first,
150 # then Change-Id, then everything else (eg. Signed-off-by:).
151 #
152 # Otherwise just print the last block, a new line and the Change-Id as a
153 # block of its own.
154 END {
155 unprinted = 1
156 if (isFooter == 0) {
157 print lines "\n"
158 lines = ""
159 }
160 changeIdAfter = "^(" tolower("'"$CHANGE_ID_AFTER"'") "):"
161 numlines = split(lines, footer, "\n")
162 for (line = 1; line <= numlines; line++) {
163 if (unprinted && match(tolower(footer[line]), changeIdAfter) != 1) {
164 unprinted = 0
165 print "Change-Id: I'"$id"'"
166 }
167 print footer[line]
168 }
169 if (unprinted) {
170 print "Change-Id: I'"$id"'"
171 }
172 }' "$MSG" > "$T" && mv "$T" "$MSG" || rm -f "$T"
173}
174_gen_ChangeIdInput() {
175 echo "tree `git write-tree`"
176 if parent=`git rev-parse "HEAD^0" 2>/dev/null`
177 then
178 echo "parent $parent"
179 fi
180 echo "author `git var GIT_AUTHOR_IDENT`"
181 echo "committer `git var GIT_COMMITTER_IDENT`"
182 echo
183 printf '%s' "$clean_message"
184}
185_gen_ChangeId() {
186 _gen_ChangeIdInput |
187 git hash-object -t commit --stdin
188}
189
190 19
191add_ChangeId 20# avoid [[ which is not POSIX sh.
21if test "$#" != 1 ; then
22 echo "$0 requires an argument."
23 exit 1
24fi
25
26if test ! -f "$1" ; then
27 echo "file does not exist: $1"
28 exit 1
29fi
30
31# Do not create a change id if requested
32if test "false" = "`git config --bool --get gerrit.createChangeId`" ; then
33 exit 0
34fi
35
36# $RANDOM will be undefined if not using bash, so don't use set -u
37random=$( (whoami ; hostname ; date; cat $1 ; echo $RANDOM) | git hash-object --stdin)
38dest="$1.tmp.${random}"
39
40trap 'rm -f "${dest}"' EXIT
41
42if ! git stripspace --strip-comments < "$1" > "${dest}" ; then
43 echo "cannot strip comments from $1"
44 exit 1
45fi
46
47if test ! -s "${dest}" ; then
48 echo "file is empty: $1"
49 exit 1
50fi
51
52# Avoid the --in-place option which only appeared in Git 2.8
53# Avoid the --if-exists option which only appeared in Git 2.15
54if ! git -c trailer.ifexists=doNothing interpret-trailers \
55 --trailer "Change-Id: I${random}" < "$1" > "${dest}" ; then
56 echo "cannot insert change-id line in $1"
57 exit 1
58fi
59
60if ! mv "${dest}" "$1" ; then
61 echo "cannot mv ${dest} to $1"
62 exit 1
63fi
diff --git a/main.py b/main.py
index 16db144f..2050cabb 100755
--- a/main.py
+++ b/main.py
@@ -1,5 +1,4 @@
1#!/usr/bin/env python 1#!/usr/bin/env python3
2# -*- coding:utf-8 -*-
3# 2#
4# Copyright (C) 2008 The Android Open Source Project 3# Copyright (C) 2008 The Android Open Source Project
5# 4#
@@ -21,23 +20,15 @@ People shouldn't run this directly; instead, they should use the `repo` wrapper
21which takes care of execing this entry point. 20which takes care of execing this entry point.
22""" 21"""
23 22
24from __future__ import print_function
25import getpass 23import getpass
26import netrc 24import netrc
27import optparse 25import optparse
28import os 26import os
27import shlex
29import sys 28import sys
30import textwrap 29import textwrap
31import time 30import time
32 31import urllib.request
33from pyversion import is_python3
34if is_python3():
35 import urllib.request
36else:
37 import imp
38 import urllib2
39 urllib = imp.new_module('urllib')
40 urllib.request = urllib2
41 32
42try: 33try:
43 import kerberos 34 import kerberos
@@ -47,8 +38,9 @@ except ImportError:
47from color import SetDefaultColoring 38from color import SetDefaultColoring
48import event_log 39import event_log
49from repo_trace import SetTrace 40from repo_trace import SetTrace
50from git_command import git, GitCommand, user_agent 41from git_command import user_agent
51from git_config import init_ssh, close_ssh 42from git_config import RepoConfig
43from git_trace2_event_log import EventLog
52from command import InteractiveCommand 44from command import InteractiveCommand
53from command import MirrorSafeCommand 45from command import MirrorSafeCommand
54from command import GitcAvailableCommand, GitcClientCommand 46from command import GitcAvailableCommand, GitcClientCommand
@@ -62,25 +54,54 @@ from error import NoManifestException
62from error import NoSuchProjectError 54from error import NoSuchProjectError
63from error import RepoChangedException 55from error import RepoChangedException
64import gitc_utils 56import gitc_utils
65from manifest_xml import GitcManifest, XmlManifest 57from manifest_xml import GitcClient, RepoClient
66from pager import RunPager, TerminatePager 58from pager import RunPager, TerminatePager
67from wrapper import WrapperPath, Wrapper 59from wrapper import WrapperPath, Wrapper
68 60
69from subcmds import all_commands 61from subcmds import all_commands
70 62
71if not is_python3(): 63
72 input = raw_input 64# NB: These do not need to be kept in sync with the repo launcher script.
65# These may be much newer as it allows the repo launcher to roll between
66# different repo releases while source versions might require a newer python.
67#
68# The soft version is when we start warning users that the version is old and
69# we'll be dropping support for it. We'll refuse to work with versions older
70# than the hard version.
71#
72# python-3.6 is in Ubuntu Bionic.
73MIN_PYTHON_VERSION_SOFT = (3, 6)
74MIN_PYTHON_VERSION_HARD = (3, 6)
75
76if sys.version_info.major < 3:
77 print('repo: error: Python 2 is no longer supported; '
78 'Please upgrade to Python {}.{}+.'.format(*MIN_PYTHON_VERSION_SOFT),
79 file=sys.stderr)
80 sys.exit(1)
81else:
82 if sys.version_info < MIN_PYTHON_VERSION_HARD:
83 print('repo: error: Python 3 version is too old; '
84 'Please upgrade to Python {}.{}+.'.format(*MIN_PYTHON_VERSION_SOFT),
85 file=sys.stderr)
86 sys.exit(1)
87 elif sys.version_info < MIN_PYTHON_VERSION_SOFT:
88 print('repo: warning: your Python 3 version is no longer supported; '
89 'Please upgrade to Python {}.{}+.'.format(*MIN_PYTHON_VERSION_SOFT),
90 file=sys.stderr)
91
73 92
74global_options = optparse.OptionParser( 93global_options = optparse.OptionParser(
75 usage='repo [-p|--paginate|--no-pager] COMMAND [ARGS]', 94 usage='repo [-p|--paginate|--no-pager] COMMAND [ARGS]',
76 add_help_option=False) 95 add_help_option=False)
77global_options.add_option('-h', '--help', action='store_true', 96global_options.add_option('-h', '--help', action='store_true',
78 help='show this help message and exit') 97 help='show this help message and exit')
98global_options.add_option('--help-all', action='store_true',
99 help='show this help message with all subcommands and exit')
79global_options.add_option('-p', '--paginate', 100global_options.add_option('-p', '--paginate',
80 dest='pager', action='store_true', 101 dest='pager', action='store_true',
81 help='display command output in the pager') 102 help='display command output in the pager')
82global_options.add_option('--no-pager', 103global_options.add_option('--no-pager',
83 dest='no_pager', action='store_true', 104 dest='pager', action='store_false',
84 help='disable the pager') 105 help='disable the pager')
85global_options.add_option('--color', 106global_options.add_option('--color',
86 choices=('auto', 'always', 'never'), default=None, 107 choices=('auto', 'always', 'never'), default=None,
@@ -97,76 +118,125 @@ global_options.add_option('--time',
97global_options.add_option('--version', 118global_options.add_option('--version',
98 dest='show_version', action='store_true', 119 dest='show_version', action='store_true',
99 help='display this version of repo') 120 help='display this version of repo')
121global_options.add_option('--show-toplevel',
122 action='store_true',
123 help='display the path of the top-level directory of '
124 'the repo client checkout')
100global_options.add_option('--event-log', 125global_options.add_option('--event-log',
101 dest='event_log', action='store', 126 dest='event_log', action='store',
102 help='filename of event log to append timeline to') 127 help='filename of event log to append timeline to')
128global_options.add_option('--git-trace2-event-log', action='store',
129 help='directory to write git trace2 event log to')
130
103 131
104class _Repo(object): 132class _Repo(object):
105 def __init__(self, repodir): 133 def __init__(self, repodir):
106 self.repodir = repodir 134 self.repodir = repodir
107 self.commands = all_commands 135 self.commands = all_commands
108 # add 'branch' as an alias for 'branches' 136
109 all_commands['branch'] = all_commands['branches'] 137 def _PrintHelp(self, short: bool = False, all_commands: bool = False):
138 """Show --help screen."""
139 global_options.print_help()
140 print()
141 if short:
142 commands = ' '.join(sorted(self.commands))
143 wrapped_commands = textwrap.wrap(commands, width=77)
144 print('Available commands:\n %s' % ('\n '.join(wrapped_commands),))
145 print('\nRun `repo help <command>` for command-specific details.')
146 print('Bug reports:', Wrapper().BUG_URL)
147 else:
148 cmd = self.commands['help']()
149 if all_commands:
150 cmd.PrintAllCommandsBody()
151 else:
152 cmd.PrintCommonCommandsBody()
110 153
111 def _ParseArgs(self, argv): 154 def _ParseArgs(self, argv):
112 """Parse the main `repo` command line options.""" 155 """Parse the main `repo` command line options."""
113 name = None 156 for i, arg in enumerate(argv):
114 glob = [] 157 if not arg.startswith('-'):
115 158 name = arg
116 for i in range(len(argv)): 159 glob = argv[:i]
117 if not argv[i].startswith('-'):
118 name = argv[i]
119 if i > 0:
120 glob = argv[:i]
121 argv = argv[i + 1:] 160 argv = argv[i + 1:]
122 break 161 break
123 if not name: 162 else:
163 name = None
124 glob = argv 164 glob = argv
125 name = 'help'
126 argv = [] 165 argv = []
127 gopts, _gargs = global_options.parse_args(glob) 166 gopts, _gargs = global_options.parse_args(glob)
128 167
129 if gopts.help: 168 if name:
130 global_options.print_help() 169 name, alias_args = self._ExpandAlias(name)
131 commands = ' '.join(sorted(self.commands)) 170 argv = alias_args + argv
132 wrapped_commands = textwrap.wrap(commands, width=77)
133 print('\nAvailable commands:\n %s' % ('\n '.join(wrapped_commands),))
134 print('\nRun `repo help <command>` for command-specific details.')
135 global_options.exit()
136 171
137 return (name, gopts, argv) 172 return (name, gopts, argv)
138 173
174 def _ExpandAlias(self, name):
175 """Look up user registered aliases."""
176 # We don't resolve aliases for existing subcommands. This matches git.
177 if name in self.commands:
178 return name, []
179
180 key = 'alias.%s' % (name,)
181 alias = RepoConfig.ForRepository(self.repodir).GetString(key)
182 if alias is None:
183 alias = RepoConfig.ForUser().GetString(key)
184 if alias is None:
185 return name, []
186
187 args = alias.strip().split(' ', 1)
188 name = args[0]
189 if len(args) == 2:
190 args = shlex.split(args[1])
191 else:
192 args = []
193 return name, args
194
139 def _Run(self, name, gopts, argv): 195 def _Run(self, name, gopts, argv):
140 """Execute the requested subcommand.""" 196 """Execute the requested subcommand."""
141 result = 0 197 result = 0
142 198
143 if gopts.trace: 199 if gopts.trace:
144 SetTrace() 200 SetTrace()
145 if gopts.show_version: 201
146 if name == 'help': 202 # Handle options that terminate quickly first.
147 name = 'version' 203 if gopts.help or gopts.help_all:
148 else: 204 self._PrintHelp(short=False, all_commands=gopts.help_all)
149 print('fatal: invalid usage of --version', file=sys.stderr) 205 return 0
150 return 1 206 elif gopts.show_version:
207 # Always allow global --version regardless of subcommand validity.
208 name = 'version'
209 elif gopts.show_toplevel:
210 print(os.path.dirname(self.repodir))
211 return 0
212 elif not name:
213 # No subcommand specified, so show the help/subcommand.
214 self._PrintHelp(short=True)
215 return 1
151 216
152 SetDefaultColoring(gopts.color) 217 SetDefaultColoring(gopts.color)
153 218
219 git_trace2_event_log = EventLog()
220 repo_client = RepoClient(self.repodir)
221 gitc_manifest = None
222 gitc_client_name = gitc_utils.parse_clientdir(os.getcwd())
223 if gitc_client_name:
224 gitc_manifest = GitcClient(self.repodir, gitc_client_name)
225 repo_client.isGitcClient = True
226
154 try: 227 try:
155 cmd = self.commands[name] 228 cmd = self.commands[name](
229 repodir=self.repodir,
230 client=repo_client,
231 manifest=repo_client.manifest,
232 gitc_manifest=gitc_manifest,
233 git_event_log=git_trace2_event_log)
156 except KeyError: 234 except KeyError:
157 print("repo: '%s' is not a repo command. See 'repo help'." % name, 235 print("repo: '%s' is not a repo command. See 'repo help'." % name,
158 file=sys.stderr) 236 file=sys.stderr)
159 return 1 237 return 1
160 238
161 cmd.repodir = self.repodir 239 Editor.globalConfig = cmd.client.globalConfig
162 cmd.manifest = XmlManifest(cmd.repodir)
163 cmd.gitc_manifest = None
164 gitc_client_name = gitc_utils.parse_clientdir(os.getcwd())
165 if gitc_client_name:
166 cmd.gitc_manifest = GitcManifest(cmd.repodir, gitc_client_name)
167 cmd.manifest.isGitcClient = True
168
169 Editor.globalConfig = cmd.manifest.globalConfig
170 240
171 if not isinstance(cmd, MirrorSafeCommand) and cmd.manifest.IsMirror: 241 if not isinstance(cmd, MirrorSafeCommand) and cmd.manifest.IsMirror:
172 print("fatal: '%s' requires a working directory" % name, 242 print("fatal: '%s' requires a working directory" % name,
@@ -188,13 +258,13 @@ class _Repo(object):
188 copts = cmd.ReadEnvironmentOptions(copts) 258 copts = cmd.ReadEnvironmentOptions(copts)
189 except NoManifestException as e: 259 except NoManifestException as e:
190 print('error: in `%s`: %s' % (' '.join([name] + argv), str(e)), 260 print('error: in `%s`: %s' % (' '.join([name] + argv), str(e)),
191 file=sys.stderr) 261 file=sys.stderr)
192 print('error: manifest missing or unreadable -- please run init', 262 print('error: manifest missing or unreadable -- please run init',
193 file=sys.stderr) 263 file=sys.stderr)
194 return 1 264 return 1
195 265
196 if not gopts.no_pager and not isinstance(cmd, InteractiveCommand): 266 if gopts.pager is not False and not isinstance(cmd, InteractiveCommand):
197 config = cmd.manifest.globalConfig 267 config = cmd.client.globalConfig
198 if gopts.pager: 268 if gopts.pager:
199 use_pager = True 269 use_pager = True
200 else: 270 else:
@@ -207,13 +277,17 @@ class _Repo(object):
207 start = time.time() 277 start = time.time()
208 cmd_event = cmd.event_log.Add(name, event_log.TASK_COMMAND, start) 278 cmd_event = cmd.event_log.Add(name, event_log.TASK_COMMAND, start)
209 cmd.event_log.SetParent(cmd_event) 279 cmd.event_log.SetParent(cmd_event)
280 git_trace2_event_log.StartEvent()
281 git_trace2_event_log.CommandEvent(name='repo', subcommands=[name])
282
210 try: 283 try:
284 cmd.CommonValidateOptions(copts, cargs)
211 cmd.ValidateOptions(copts, cargs) 285 cmd.ValidateOptions(copts, cargs)
212 result = cmd.Execute(copts, cargs) 286 result = cmd.Execute(copts, cargs)
213 except (DownloadError, ManifestInvalidRevisionError, 287 except (DownloadError, ManifestInvalidRevisionError,
214 NoManifestException) as e: 288 NoManifestException) as e:
215 print('error: in `%s`: %s' % (' '.join([name] + argv), str(e)), 289 print('error: in `%s`: %s' % (' '.join([name] + argv), str(e)),
216 file=sys.stderr) 290 file=sys.stderr)
217 if isinstance(e, NoManifestException): 291 if isinstance(e, NoManifestException):
218 print('error: manifest missing or unreadable -- please run init', 292 print('error: manifest missing or unreadable -- please run init',
219 file=sys.stderr) 293 file=sys.stderr)
@@ -228,7 +302,8 @@ class _Repo(object):
228 if e.name: 302 if e.name:
229 print('error: project group must be enabled for project %s' % e.name, file=sys.stderr) 303 print('error: project group must be enabled for project %s' % e.name, file=sys.stderr)
230 else: 304 else:
231 print('error: project group must be enabled for the project in the current directory', file=sys.stderr) 305 print('error: project group must be enabled for the project in the current directory',
306 file=sys.stderr)
232 result = 1 307 result = 1
233 except SystemExit as e: 308 except SystemExit as e:
234 if e.code: 309 if e.code:
@@ -248,49 +323,78 @@ class _Repo(object):
248 323
249 cmd.event_log.FinishEvent(cmd_event, finish, 324 cmd.event_log.FinishEvent(cmd_event, finish,
250 result is None or result == 0) 325 result is None or result == 0)
326 git_trace2_event_log.DefParamRepoEvents(
327 cmd.manifest.manifestProject.config.DumpConfigDict())
328 git_trace2_event_log.ExitEvent(result)
329
251 if gopts.event_log: 330 if gopts.event_log:
252 cmd.event_log.Write(os.path.abspath( 331 cmd.event_log.Write(os.path.abspath(
253 os.path.expanduser(gopts.event_log))) 332 os.path.expanduser(gopts.event_log)))
254 333
334 git_trace2_event_log.Write(gopts.git_trace2_event_log)
255 return result 335 return result
256 336
257 337
258def _CheckWrapperVersion(ver, repo_path): 338def _CheckWrapperVersion(ver_str, repo_path):
339 """Verify the repo launcher is new enough for this checkout.
340
341 Args:
342 ver_str: The version string passed from the repo launcher when it ran us.
343 repo_path: The path to the repo launcher that loaded us.
344 """
345 # Refuse to work with really old wrapper versions. We don't test these,
346 # so might as well require a somewhat recent sane version.
347 # v1.15 of the repo launcher was released in ~Mar 2012.
348 MIN_REPO_VERSION = (1, 15)
349 min_str = '.'.join(str(x) for x in MIN_REPO_VERSION)
350
259 if not repo_path: 351 if not repo_path:
260 repo_path = '~/bin/repo' 352 repo_path = '~/bin/repo'
261 353
262 if not ver: 354 if not ver_str:
263 print('no --wrapper-version argument', file=sys.stderr) 355 print('no --wrapper-version argument', file=sys.stderr)
264 sys.exit(1) 356 sys.exit(1)
265 357
358 # Pull out the version of the repo launcher we know about to compare.
266 exp = Wrapper().VERSION 359 exp = Wrapper().VERSION
267 ver = tuple(map(int, ver.split('.'))) 360 ver = tuple(map(int, ver_str.split('.')))
268 if len(ver) == 1:
269 ver = (0, ver[0])
270 361
271 exp_str = '.'.join(map(str, exp)) 362 exp_str = '.'.join(map(str, exp))
272 if exp[0] > ver[0] or ver < (0, 4): 363 if ver < MIN_REPO_VERSION:
273 print(""" 364 print("""
274!!! A new repo command (%5s) is available. !!! 365repo: error:
275!!! You must upgrade before you can continue: !!! 366!!! Your version of repo %s is too old.
367!!! We need at least version %s.
368!!! A new version of repo (%s) is available.
369!!! You must upgrade before you can continue:
276 370
277 cp %s %s 371 cp %s %s
278""" % (exp_str, WrapperPath(), repo_path), file=sys.stderr) 372""" % (ver_str, min_str, exp_str, WrapperPath(), repo_path), file=sys.stderr)
279 sys.exit(1) 373 sys.exit(1)
280 374
281 if exp > ver: 375 if exp > ver:
282 print(""" 376 print('\n... A new version of repo (%s) is available.' % (exp_str,),
283... A new repo command (%5s) is available. 377 file=sys.stderr)
378 if os.access(repo_path, os.W_OK):
379 print("""\
284... You should upgrade soon: 380... You should upgrade soon:
285
286 cp %s %s 381 cp %s %s
287""" % (exp_str, WrapperPath(), repo_path), file=sys.stderr) 382""" % (WrapperPath(), repo_path), file=sys.stderr)
383 else:
384 print("""\
385... New version is available at: %s
386... The launcher is run from: %s
387!!! The launcher is not writable. Please talk to your sysadmin or distro
388!!! to get an update installed.
389""" % (WrapperPath(), repo_path), file=sys.stderr)
390
288 391
289def _CheckRepoDir(repo_dir): 392def _CheckRepoDir(repo_dir):
290 if not repo_dir: 393 if not repo_dir:
291 print('no --repo-dir argument', file=sys.stderr) 394 print('no --repo-dir argument', file=sys.stderr)
292 sys.exit(1) 395 sys.exit(1)
293 396
397
294def _PruneOptions(argv, opt): 398def _PruneOptions(argv, opt):
295 i = 0 399 i = 0
296 while i < len(argv): 400 while i < len(argv):
@@ -306,6 +410,7 @@ def _PruneOptions(argv, opt):
306 continue 410 continue
307 i += 1 411 i += 1
308 412
413
309class _UserAgentHandler(urllib.request.BaseHandler): 414class _UserAgentHandler(urllib.request.BaseHandler):
310 def http_request(self, req): 415 def http_request(self, req):
311 req.add_header('User-Agent', user_agent.repo) 416 req.add_header('User-Agent', user_agent.repo)
@@ -315,6 +420,7 @@ class _UserAgentHandler(urllib.request.BaseHandler):
315 req.add_header('User-Agent', user_agent.repo) 420 req.add_header('User-Agent', user_agent.repo)
316 return req 421 return req
317 422
423
318def _AddPasswordFromUserInput(handler, msg, req): 424def _AddPasswordFromUserInput(handler, msg, req):
319 # If repo could not find auth info from netrc, try to get it from user input 425 # If repo could not find auth info from netrc, try to get it from user input
320 url = req.get_full_url() 426 url = req.get_full_url()
@@ -328,22 +434,24 @@ def _AddPasswordFromUserInput(handler, msg, req):
328 return 434 return
329 handler.passwd.add_password(None, url, user, password) 435 handler.passwd.add_password(None, url, user, password)
330 436
437
331class _BasicAuthHandler(urllib.request.HTTPBasicAuthHandler): 438class _BasicAuthHandler(urllib.request.HTTPBasicAuthHandler):
332 def http_error_401(self, req, fp, code, msg, headers): 439 def http_error_401(self, req, fp, code, msg, headers):
333 _AddPasswordFromUserInput(self, msg, req) 440 _AddPasswordFromUserInput(self, msg, req)
334 return urllib.request.HTTPBasicAuthHandler.http_error_401( 441 return urllib.request.HTTPBasicAuthHandler.http_error_401(
335 self, req, fp, code, msg, headers) 442 self, req, fp, code, msg, headers)
336 443
337 def http_error_auth_reqed(self, authreq, host, req, headers): 444 def http_error_auth_reqed(self, authreq, host, req, headers):
338 try: 445 try:
339 old_add_header = req.add_header 446 old_add_header = req.add_header
447
340 def _add_header(name, val): 448 def _add_header(name, val):
341 val = val.replace('\n', '') 449 val = val.replace('\n', '')
342 old_add_header(name, val) 450 old_add_header(name, val)
343 req.add_header = _add_header 451 req.add_header = _add_header
344 return urllib.request.AbstractBasicAuthHandler.http_error_auth_reqed( 452 return urllib.request.AbstractBasicAuthHandler.http_error_auth_reqed(
345 self, authreq, host, req, headers) 453 self, authreq, host, req, headers)
346 except: 454 except Exception:
347 reset = getattr(self, 'reset_retry_count', None) 455 reset = getattr(self, 'reset_retry_count', None)
348 if reset is not None: 456 if reset is not None:
349 reset() 457 reset()
@@ -351,22 +459,24 @@ class _BasicAuthHandler(urllib.request.HTTPBasicAuthHandler):
351 self.retried = 0 459 self.retried = 0
352 raise 460 raise
353 461
462
354class _DigestAuthHandler(urllib.request.HTTPDigestAuthHandler): 463class _DigestAuthHandler(urllib.request.HTTPDigestAuthHandler):
355 def http_error_401(self, req, fp, code, msg, headers): 464 def http_error_401(self, req, fp, code, msg, headers):
356 _AddPasswordFromUserInput(self, msg, req) 465 _AddPasswordFromUserInput(self, msg, req)
357 return urllib.request.HTTPDigestAuthHandler.http_error_401( 466 return urllib.request.HTTPDigestAuthHandler.http_error_401(
358 self, req, fp, code, msg, headers) 467 self, req, fp, code, msg, headers)
359 468
360 def http_error_auth_reqed(self, auth_header, host, req, headers): 469 def http_error_auth_reqed(self, auth_header, host, req, headers):
361 try: 470 try:
362 old_add_header = req.add_header 471 old_add_header = req.add_header
472
363 def _add_header(name, val): 473 def _add_header(name, val):
364 val = val.replace('\n', '') 474 val = val.replace('\n', '')
365 old_add_header(name, val) 475 old_add_header(name, val)
366 req.add_header = _add_header 476 req.add_header = _add_header
367 return urllib.request.AbstractDigestAuthHandler.http_error_auth_reqed( 477 return urllib.request.AbstractDigestAuthHandler.http_error_auth_reqed(
368 self, auth_header, host, req, headers) 478 self, auth_header, host, req, headers)
369 except: 479 except Exception:
370 reset = getattr(self, 'reset_retry_count', None) 480 reset = getattr(self, 'reset_retry_count', None)
371 if reset is not None: 481 if reset is not None:
372 reset() 482 reset()
@@ -374,6 +484,7 @@ class _DigestAuthHandler(urllib.request.HTTPDigestAuthHandler):
374 self.retried = 0 484 self.retried = 0
375 raise 485 raise
376 486
487
377class _KerberosAuthHandler(urllib.request.BaseHandler): 488class _KerberosAuthHandler(urllib.request.BaseHandler):
378 def __init__(self): 489 def __init__(self):
379 self.retried = 0 490 self.retried = 0
@@ -392,7 +503,7 @@ class _KerberosAuthHandler(urllib.request.BaseHandler):
392 503
393 if self.retried > 3: 504 if self.retried > 3:
394 raise urllib.request.HTTPError(req.get_full_url(), 401, 505 raise urllib.request.HTTPError(req.get_full_url(), 401,
395 "Negotiate auth failed", headers, None) 506 "Negotiate auth failed", headers, None)
396 else: 507 else:
397 self.retried += 1 508 self.retried += 1
398 509
@@ -408,7 +519,7 @@ class _KerberosAuthHandler(urllib.request.BaseHandler):
408 return response 519 return response
409 except kerberos.GSSError: 520 except kerberos.GSSError:
410 return None 521 return None
411 except: 522 except Exception:
412 self.reset_retry_count() 523 self.reset_retry_count()
413 raise 524 raise
414 finally: 525 finally:
@@ -454,6 +565,7 @@ class _KerberosAuthHandler(urllib.request.BaseHandler):
454 kerberos.authGSSClientClean(self.context) 565 kerberos.authGSSClientClean(self.context)
455 self.context = None 566 self.context = None
456 567
568
457def init_http(): 569def init_http():
458 handlers = [_UserAgentHandler()] 570 handlers = [_UserAgentHandler()]
459 571
@@ -462,7 +574,7 @@ def init_http():
462 n = netrc.netrc() 574 n = netrc.netrc()
463 for host in n.hosts: 575 for host in n.hosts:
464 p = n.hosts[host] 576 p = n.hosts[host]
465 mgr.add_password(p[1], 'http://%s/' % host, p[0], p[2]) 577 mgr.add_password(p[1], 'http://%s/' % host, p[0], p[2])
466 mgr.add_password(p[1], 'https://%s/' % host, p[0], p[2]) 578 mgr.add_password(p[1], 'https://%s/' % host, p[0], p[2])
467 except netrc.NetrcParseError: 579 except netrc.NetrcParseError:
468 pass 580 pass
@@ -481,6 +593,7 @@ def init_http():
481 handlers.append(urllib.request.HTTPSHandler(debuglevel=1)) 593 handlers.append(urllib.request.HTTPSHandler(debuglevel=1))
482 urllib.request.install_opener(urllib.request.build_opener(*handlers)) 594 urllib.request.install_opener(urllib.request.build_opener(*handlers))
483 595
596
484def _Main(argv): 597def _Main(argv):
485 result = 0 598 result = 0
486 599
@@ -502,20 +615,16 @@ def _Main(argv):
502 615
503 repo = _Repo(opt.repodir) 616 repo = _Repo(opt.repodir)
504 try: 617 try:
505 try: 618 init_http()
506 init_ssh() 619 name, gopts, argv = repo._ParseArgs(argv)
507 init_http() 620 run = lambda: repo._Run(name, gopts, argv) or 0
508 name, gopts, argv = repo._ParseArgs(argv) 621 if gopts.trace_python:
509 run = lambda: repo._Run(name, gopts, argv) or 0 622 import trace
510 if gopts.trace_python: 623 tracer = trace.Trace(count=False, trace=True, timing=True,
511 import trace 624 ignoredirs=set(sys.path[1:]))
512 tracer = trace.Trace(count=False, trace=True, timing=True, 625 result = tracer.runfunc(run)
513 ignoredirs=set(sys.path[1:])) 626 else:
514 result = tracer.runfunc(run) 627 result = run()
515 else:
516 result = run()
517 finally:
518 close_ssh()
519 except KeyboardInterrupt: 628 except KeyboardInterrupt:
520 print('aborted by user', file=sys.stderr) 629 print('aborted by user', file=sys.stderr)
521 result = 1 630 result = 1
@@ -528,7 +637,7 @@ def _Main(argv):
528 argv = list(sys.argv) 637 argv = list(sys.argv)
529 argv.extend(rce.extra_args) 638 argv.extend(rce.extra_args)
530 try: 639 try:
531 os.execv(__file__, argv) 640 os.execv(sys.executable, [__file__] + argv)
532 except OSError as e: 641 except OSError as e:
533 print('fatal: cannot restart repo after upgrade', file=sys.stderr) 642 print('fatal: cannot restart repo after upgrade', file=sys.stderr)
534 print('fatal: %s' % e, file=sys.stderr) 643 print('fatal: %s' % e, file=sys.stderr)
@@ -537,5 +646,6 @@ def _Main(argv):
537 TerminatePager() 646 TerminatePager()
538 sys.exit(result) 647 sys.exit(result)
539 648
649
540if __name__ == '__main__': 650if __name__ == '__main__':
541 _Main(sys.argv[1:]) 651 _Main(sys.argv[1:])
diff --git a/man/repo-abandon.1 b/man/repo-abandon.1
new file mode 100644
index 00000000..b3c0422f
--- /dev/null
+++ b/man/repo-abandon.1
@@ -0,0 +1,36 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo abandon" "Repo Manual"
3.SH NAME
4repo \- repo abandon - manual page for repo abandon
5.SH SYNOPSIS
6.B repo
7\fI\,abandon \/\fR[\fI\,--all | <branchname>\/\fR] [\fI\,<project>\/\fR...]
8.SH DESCRIPTION
9Summary
10.PP
11Permanently abandon a development branch
12.PP
13This subcommand permanently abandons a development branch by
14deleting it (and all its history) from your local repository.
15.PP
16It is equivalent to "git branch \fB\-D\fR <branchname>".
17.SH OPTIONS
18.TP
19\fB\-h\fR, \fB\-\-help\fR
20show this help message and exit
21.TP
22\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
23number of jobs to run in parallel (default: based on
24number of CPU cores)
25.TP
26\fB\-\-all\fR
27delete all branches in all projects
28.SS Logging options:
29.TP
30\fB\-v\fR, \fB\-\-verbose\fR
31show all output
32.TP
33\fB\-q\fR, \fB\-\-quiet\fR
34only show errors
35.PP
36Run `repo help abandon` to view the detailed manual.
diff --git a/man/repo-branch.1 b/man/repo-branch.1
new file mode 100644
index 00000000..854ee98b
--- /dev/null
+++ b/man/repo-branch.1
@@ -0,0 +1 @@
.so man1/repo-branches.1 \ No newline at end of file
diff --git a/man/repo-branches.1 b/man/repo-branches.1
new file mode 100644
index 00000000..7fe0b02d
--- /dev/null
+++ b/man/repo-branches.1
@@ -0,0 +1,59 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo branches" "Repo Manual"
3.SH NAME
4repo \- repo branches - manual page for repo branches
5.SH SYNOPSIS
6.B repo
7\fI\,branches \/\fR[\fI\,<project>\/\fR...]
8.SH DESCRIPTION
9Summary
10.PP
11View current topic branches
12.PP
13Summarizes the currently available topic branches.
14.PP
15# Branch Display
16.PP
17The branch display output by this command is organized into four
18columns of information; for example:
19.TP
20*P nocolor
21| in repo
22.TP
23repo2
24|
25.PP
26The first column contains a * if the branch is the currently
27checked out branch in any of the specified projects, or a blank
28if no project has the branch checked out.
29.PP
30The second column contains either blank, p or P, depending upon
31the upload status of the branch.
32.IP
33(blank): branch not yet published by repo upload
34.IP
35P: all commits were published by repo upload
36p: only some commits were published by repo upload
37.PP
38The third column contains the branch name.
39.PP
40The fourth column (after the | separator) lists the projects that
41the branch appears in, or does not appear in. If no project list
42is shown, then the branch appears in all projects.
43.SH OPTIONS
44.TP
45\fB\-h\fR, \fB\-\-help\fR
46show this help message and exit
47.TP
48\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
49number of jobs to run in parallel (default: based on
50number of CPU cores)
51.SS Logging options:
52.TP
53\fB\-v\fR, \fB\-\-verbose\fR
54show all output
55.TP
56\fB\-q\fR, \fB\-\-quiet\fR
57only show errors
58.PP
59Run `repo help branches` to view the detailed manual.
diff --git a/man/repo-checkout.1 b/man/repo-checkout.1
new file mode 100644
index 00000000..6dd3e6ca
--- /dev/null
+++ b/man/repo-checkout.1
@@ -0,0 +1,36 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo checkout" "Repo Manual"
3.SH NAME
4repo \- repo checkout - manual page for repo checkout
5.SH SYNOPSIS
6.B repo
7\fI\,checkout <branchname> \/\fR[\fI\,<project>\/\fR...]
8.SH DESCRIPTION
9Summary
10.PP
11Checkout a branch for development
12.SH OPTIONS
13.TP
14\fB\-h\fR, \fB\-\-help\fR
15show this help message and exit
16.TP
17\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
18number of jobs to run in parallel (default: based on
19number of CPU cores)
20.SS Logging options:
21.TP
22\fB\-v\fR, \fB\-\-verbose\fR
23show all output
24.TP
25\fB\-q\fR, \fB\-\-quiet\fR
26only show errors
27.PP
28Run `repo help checkout` to view the detailed manual.
29.SH DETAILS
30.PP
31The 'repo checkout' command checks out an existing branch that was previously
32created by 'repo start'.
33.PP
34The command is equivalent to:
35.IP
36repo forall [<project>...] \fB\-c\fR git checkout <branchname>
diff --git a/man/repo-cherry-pick.1 b/man/repo-cherry-pick.1
new file mode 100644
index 00000000..e7716c55
--- /dev/null
+++ b/man/repo-cherry-pick.1
@@ -0,0 +1,28 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo cherry-pick" "Repo Manual"
3.SH NAME
4repo \- repo cherry-pick - manual page for repo cherry-pick
5.SH SYNOPSIS
6.B repo
7\fI\,cherry-pick <sha1>\/\fR
8.SH DESCRIPTION
9Summary
10.PP
11Cherry\-pick a change.
12.SH OPTIONS
13.TP
14\fB\-h\fR, \fB\-\-help\fR
15show this help message and exit
16.SS Logging options:
17.TP
18\fB\-v\fR, \fB\-\-verbose\fR
19show all output
20.TP
21\fB\-q\fR, \fB\-\-quiet\fR
22only show errors
23.PP
24Run `repo help cherry\-pick` to view the detailed manual.
25.SH DETAILS
26.PP
27\&'repo cherry\-pick' cherry\-picks a change from one branch to another. The change
28id will be updated, and a reference to the old change id will be added.
diff --git a/man/repo-diff.1 b/man/repo-diff.1
new file mode 100644
index 00000000..890f8d22
--- /dev/null
+++ b/man/repo-diff.1
@@ -0,0 +1,35 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo diff" "Repo Manual"
3.SH NAME
4repo \- repo diff - manual page for repo diff
5.SH SYNOPSIS
6.B repo
7\fI\,diff \/\fR[\fI\,<project>\/\fR...]
8.SH DESCRIPTION
9Summary
10.PP
11Show changes between commit and working tree
12.PP
13The \fB\-u\fR option causes 'repo diff' to generate diff output with file paths
14relative to the repository root, so the output can be applied
15to the Unix 'patch' command.
16.SH OPTIONS
17.TP
18\fB\-h\fR, \fB\-\-help\fR
19show this help message and exit
20.TP
21\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
22number of jobs to run in parallel (default: based on
23number of CPU cores)
24.TP
25\fB\-u\fR, \fB\-\-absolute\fR
26paths are relative to the repository root
27.SS Logging options:
28.TP
29\fB\-v\fR, \fB\-\-verbose\fR
30show all output
31.TP
32\fB\-q\fR, \fB\-\-quiet\fR
33only show errors
34.PP
35Run `repo help diff` to view the detailed manual.
diff --git a/man/repo-diffmanifests.1 b/man/repo-diffmanifests.1
new file mode 100644
index 00000000..add50f17
--- /dev/null
+++ b/man/repo-diffmanifests.1
@@ -0,0 +1,61 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo diffmanifests" "Repo Manual"
3.SH NAME
4repo \- 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
9Summary
10.PP
11Manifest diff utility
12.SH OPTIONS
13.TP
14\fB\-h\fR, \fB\-\-help\fR
15show this help message and exit
16.TP
17\fB\-\-raw\fR
18display raw diff
19.TP
20\fB\-\-no\-color\fR
21does not display the diff in color
22.TP
23\fB\-\-pretty\-format=\fR<FORMAT>
24print the log using a custom git pretty format string
25.SS Logging options:
26.TP
27\fB\-v\fR, \fB\-\-verbose\fR
28show all output
29.TP
30\fB\-q\fR, \fB\-\-quiet\fR
31only show errors
32.PP
33Run `repo help diffmanifests` to view the detailed manual.
34.SH DETAILS
35.PP
36The repo diffmanifests command shows differences between project revisions of
37manifest1 and manifest2. if manifest2 is not specified, current manifest.xml
38will be used instead. Both absolute and relative paths may be used for
39manifests. Relative paths start from project's ".repo/manifests" folder.
40.PP
41The \fB\-\-raw\fR option Displays the diff in a way that facilitates parsing, the
42project pattern will be <status> <path> <revision from> [<revision to>] and the
43commit pattern will be <status> <onelined log> with status values respectively :
44.IP
45A = Added project
46R = Removed project
47C = Changed project
48U = Project with unreachable revision(s) (revision(s) not found)
49.PP
50for project, and
51.IP
52A = Added commit
53R = Removed commit
54.PP
55for a commit.
56.PP
57Only changed projects may contain commits, and commit status always starts with
58a space, and are part of last printed project. Unreachable revisions may occur
59if project is not up to date or if repo has not been initialized with all the
60groups, in which case some projects won't be synced and their revisions won't be
61found.
diff --git a/man/repo-download.1 b/man/repo-download.1
new file mode 100644
index 00000000..cf7f767d
--- /dev/null
+++ b/man/repo-download.1
@@ -0,0 +1,44 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo download" "Repo Manual"
3.SH NAME
4repo \- 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
9Summary
10.PP
11Download and checkout a change
12.SH OPTIONS
13.TP
14\fB\-h\fR, \fB\-\-help\fR
15show this help message and exit
16.TP
17\fB\-b\fR BRANCH, \fB\-\-branch\fR=\fI\,BRANCH\/\fR
18create a new branch first
19.TP
20\fB\-c\fR, \fB\-\-cherry\-pick\fR
21cherry\-pick instead of checkout
22.TP
23\fB\-x\fR, \fB\-\-record\-origin\fR
24pass \fB\-x\fR when cherry\-picking
25.TP
26\fB\-r\fR, \fB\-\-revert\fR
27revert instead of checkout
28.TP
29\fB\-f\fR, \fB\-\-ff\-only\fR
30force fast\-forward merge
31.SS Logging options:
32.TP
33\fB\-v\fR, \fB\-\-verbose\fR
34show all output
35.TP
36\fB\-q\fR, \fB\-\-quiet\fR
37only show errors
38.PP
39Run `repo help download` to view the detailed manual.
40.SH DETAILS
41.PP
42The 'repo download' command downloads a change from the review system and makes
43it available in your project's local working directory. If no project is
44specified try to use current directory as a project.
diff --git a/man/repo-forall.1 b/man/repo-forall.1
new file mode 100644
index 00000000..eb2ad57b
--- /dev/null
+++ b/man/repo-forall.1
@@ -0,0 +1,128 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo forall" "Repo Manual"
3.SH NAME
4repo \- repo forall - manual page for repo forall
5.SH SYNOPSIS
6.B repo
7\fI\,forall \/\fR[\fI\,<project>\/\fR...] \fI\,-c <command> \/\fR[\fI\,<arg>\/\fR...]
8.SH DESCRIPTION
9Summary
10.PP
11Run a shell command in each project
12.PP
13repo forall \fB\-r\fR str1 [str2] ... \fB\-c\fR <command> [<arg>...]
14.SH OPTIONS
15.TP
16\fB\-h\fR, \fB\-\-help\fR
17show this help message and exit
18.TP
19\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
20number of jobs to run in parallel (default: based on
21number of CPU cores)
22.TP
23\fB\-r\fR, \fB\-\-regex\fR
24execute the command only on projects matching regex or
25wildcard expression
26.TP
27\fB\-i\fR, \fB\-\-inverse\-regex\fR
28execute the command only on projects not matching
29regex or wildcard expression
30.TP
31\fB\-g\fR GROUPS, \fB\-\-groups\fR=\fI\,GROUPS\/\fR
32execute the command only on projects matching the
33specified groups
34.TP
35\fB\-c\fR, \fB\-\-command\fR
36command (and arguments) to execute
37.TP
38\fB\-e\fR, \fB\-\-abort\-on\-errors\fR
39abort if a command exits unsuccessfully
40.TP
41\fB\-\-ignore\-missing\fR
42silently skip & do not exit non\-zero due missing
43checkouts
44.TP
45\fB\-\-interactive\fR
46force interactive usage
47.SS Logging options:
48.TP
49\fB\-v\fR, \fB\-\-verbose\fR
50show all output
51.TP
52\fB\-q\fR, \fB\-\-quiet\fR
53only show errors
54.TP
55\fB\-p\fR
56show project headers before output
57.PP
58Run `repo help forall` to view the detailed manual.
59.SH DETAILS
60.PP
61Executes the same shell command in each project.
62.PP
63The \fB\-r\fR option allows running the command only on projects matching regex or
64wildcard expression.
65.PP
66By default, projects are processed non\-interactively in parallel. If you want to
67run interactive commands, make sure to pass \fB\-\-interactive\fR to force \fB\-\-jobs\fR 1.
68While the processing order of projects is not guaranteed, the order of project
69output is stable.
70.PP
71Output Formatting
72.PP
73The \fB\-p\fR option causes 'repo forall' to bind pipes to the command's stdin, stdout
74and stderr streams, and pipe all output into a continuous stream that is
75displayed in a single pager session. Project headings are inserted before the
76output of each command is displayed. If the command produces no output in a
77project, no heading is displayed.
78.PP
79The formatting convention used by \fB\-p\fR is very suitable for some types of
80searching, e.g. `repo forall \fB\-p\fR \fB\-c\fR git log \fB\-SFoo\fR` will print all commits that
81add or remove references to Foo.
82.PP
83The \fB\-v\fR option causes 'repo forall' to display stderr messages if a command
84produces output only on stderr. Normally the \fB\-p\fR option causes command output to
85be suppressed until the command produces at least one byte of output on stdout.
86.PP
87Environment
88.PP
89pwd is the project's working directory. If the current client is a mirror
90client, then pwd is the Git repository.
91.PP
92REPO_PROJECT is set to the unique name of the project.
93.PP
94REPO_PATH is the path relative the the root of the client.
95.PP
96REPO_REMOTE is the name of the remote system from the manifest.
97.PP
98REPO_LREV is the name of the revision from the manifest, translated to a local
99tracking branch. If you need to pass the manifest revision to a locally executed
100git command, use REPO_LREV.
101.PP
102REPO_RREV is the name of the revision from the manifest, exactly as written in
103the manifest.
104.PP
105REPO_COUNT is the total number of projects being iterated.
106.PP
107REPO_I is the current (1\-based) iteration count. Can be used in conjunction with
108REPO_COUNT to add a simple progress indicator to your command.
109.PP
110REPO__* are any extra environment variables, specified by the "annotation"
111element under any project element. This can be useful for differentiating trees
112based on user\-specific criteria, or simply annotating tree details.
113.PP
114shell positional arguments ($1, $2, .., $#) are set to any arguments following
115<command>.
116.PP
117Example: to list projects:
118.IP
119repo forall \fB\-c\fR 'echo $REPO_PROJECT'
120.PP
121Notice that $REPO_PROJECT is quoted to ensure it is expanded in the context of
122running <command> instead of in the calling shell.
123.PP
124Unless \fB\-p\fR is used, stdin, stdout, stderr are inherited from the terminal and are
125not redirected.
126.PP
127If \fB\-e\fR is used, when a command exits unsuccessfully, 'repo forall' will abort
128without iterating through the remaining projects.
diff --git a/man/repo-gitc-delete.1 b/man/repo-gitc-delete.1
new file mode 100644
index 00000000..c84c6e45
--- /dev/null
+++ b/man/repo-gitc-delete.1
@@ -0,0 +1,31 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo gitc-delete" "Repo Manual"
3.SH NAME
4repo \- repo gitc-delete - manual page for repo gitc-delete
5.SH SYNOPSIS
6.B repo
7\fI\,gitc-delete\/\fR
8.SH DESCRIPTION
9Summary
10.PP
11Delete a GITC Client.
12.SH OPTIONS
13.TP
14\fB\-h\fR, \fB\-\-help\fR
15show this help message and exit
16.TP
17\fB\-f\fR, \fB\-\-force\fR
18force the deletion (no prompt)
19.SS Logging options:
20.TP
21\fB\-v\fR, \fB\-\-verbose\fR
22show all output
23.TP
24\fB\-q\fR, \fB\-\-quiet\fR
25only show errors
26.PP
27Run `repo help gitc\-delete` to view the detailed manual.
28.SH DETAILS
29.PP
30This subcommand deletes the current GITC client, deleting the GITC manifest and
31all locally downloaded sources.
diff --git a/man/repo-gitc-init.1 b/man/repo-gitc-init.1
new file mode 100644
index 00000000..9b61866e
--- /dev/null
+++ b/man/repo-gitc-init.1
@@ -0,0 +1,150 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "September 2021" "repo gitc-init" "Repo Manual"
3.SH NAME
4repo \- repo gitc-init - manual page for repo gitc-init
5.SH SYNOPSIS
6.B repo
7\fI\,gitc-init \/\fR[\fI\,options\/\fR] [\fI\,client name\/\fR]
8.SH DESCRIPTION
9Summary
10.PP
11Initialize a GITC Client.
12.SH OPTIONS
13.TP
14\fB\-h\fR, \fB\-\-help\fR
15show this help message and exit
16.SS Logging options:
17.TP
18\fB\-v\fR, \fB\-\-verbose\fR
19show all output
20.TP
21\fB\-q\fR, \fB\-\-quiet\fR
22only show errors
23.SS Manifest options:
24.TP
25\fB\-u\fR URL, \fB\-\-manifest\-url\fR=\fI\,URL\/\fR
26manifest repository location
27.TP
28\fB\-b\fR REVISION, \fB\-\-manifest\-branch\fR=\fI\,REVISION\/\fR
29manifest branch or revision (use HEAD for default)
30.TP
31\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
32initial manifest file
33.TP
34\fB\-\-standalone\-manifest\fR
35download the manifest as a static file rather then
36create a git checkout of the manifest repo
37.TP
38\fB\-g\fR GROUP, \fB\-\-groups\fR=\fI\,GROUP\/\fR
39restrict manifest projects to ones with specified
40group(s) [default|all|G1,G2,G3|G4,\-G5,\-G6]
41.TP
42\fB\-p\fR PLATFORM, \fB\-\-platform\fR=\fI\,PLATFORM\/\fR
43restrict manifest projects to ones with a specified
44platform group [auto|all|none|linux|darwin|...]
45.TP
46\fB\-\-submodules\fR
47sync any submodules associated with the manifest repo
48.SS Manifest (only) checkout options:
49.TP
50\fB\-\-current\-branch\fR
51fetch only current manifest branch from server
52.TP
53\fB\-\-no\-current\-branch\fR
54fetch all manifest branches from server
55.TP
56\fB\-\-tags\fR
57fetch tags in the manifest
58.TP
59\fB\-\-no\-tags\fR
60don't fetch tags in the manifest
61.SS Checkout modes:
62.TP
63\fB\-\-mirror\fR
64create a replica of the remote repositories rather
65than a client working directory
66.TP
67\fB\-\-archive\fR
68checkout an archive instead of a git repository for
69each project. See git archive.
70.TP
71\fB\-\-worktree\fR
72use git\-worktree to manage projects
73.SS Project checkout optimizations:
74.TP
75\fB\-\-reference\fR=\fI\,DIR\/\fR
76location of mirror directory
77.TP
78\fB\-\-dissociate\fR
79dissociate from reference mirrors after clone
80.TP
81\fB\-\-depth\fR=\fI\,DEPTH\/\fR
82create a shallow clone with given depth; see git clone
83.TP
84\fB\-\-partial\-clone\fR
85perform partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code)
86.TP
87\fB\-\-no\-partial\-clone\fR
88disable use of partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code)
89.TP
90\fB\-\-partial\-clone\-exclude\fR=\fI\,PARTIAL_CLONE_EXCLUDE\/\fR
91exclude the specified projects (a comma\-delimited
92project names) from partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code)
93.TP
94\fB\-\-clone\-filter\fR=\fI\,CLONE_FILTER\/\fR
95filter for use with \fB\-\-partial\-clone\fR [default:
96blob:none]
97.TP
98\fB\-\-use\-superproject\fR
99use the manifest superproject to sync projects
100.TP
101\fB\-\-no\-use\-superproject\fR
102disable use of manifest superprojects
103.TP
104\fB\-\-clone\-bundle\fR
105enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS (default if
106not \fB\-\-partial\-clone\fR)
107.TP
108\fB\-\-no\-clone\-bundle\fR
109disable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS (default if
110\fB\-\-partial\-clone\fR)
111.SS repo Version options:
112.TP
113\fB\-\-repo\-url\fR=\fI\,URL\/\fR
114repo repository location ($REPO_URL)
115.TP
116\fB\-\-repo\-rev\fR=\fI\,REV\/\fR
117repo branch or revision ($REPO_REV)
118.TP
119\fB\-\-no\-repo\-verify\fR
120do not verify repo source code
121.SS Other options:
122.TP
123\fB\-\-config\-name\fR
124Always prompt for name/e\-mail
125.SS GITC options:
126.TP
127\fB\-f\fR MANIFEST_FILE, \fB\-\-manifest\-file\fR=\fI\,MANIFEST_FILE\/\fR
128Optional manifest file to use for this GITC client.
129.TP
130\fB\-c\fR GITC_CLIENT, \fB\-\-gitc\-client\fR=\fI\,GITC_CLIENT\/\fR
131Name of the gitc_client instance to create or modify.
132.PP
133Run `repo help gitc\-init` to view the detailed manual.
134.SH DETAILS
135.PP
136The 'repo gitc\-init' command is ran to initialize a new GITC client for use with
137the GITC file system.
138.PP
139This command will setup the client directory, initialize repo, just like repo
140init does, and then downloads the manifest collection and installs it in the
141\&.repo/directory of the GITC client.
142.PP
143Once this is done, a GITC manifest is generated by pulling the HEAD SHA for each
144project and generates the properly formatted XML file and installs it as
145\&.manifest in the GITC client directory.
146.PP
147The \fB\-c\fR argument is required to specify the GITC client name.
148.PP
149The optional \fB\-f\fR argument can be used to specify the manifest file to use for
150this GITC client.
diff --git a/man/repo-grep.1 b/man/repo-grep.1
new file mode 100644
index 00000000..be410588
--- /dev/null
+++ b/man/repo-grep.1
@@ -0,0 +1,119 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo grep" "Repo Manual"
3.SH NAME
4repo \- repo grep - manual page for repo grep
5.SH SYNOPSIS
6.B repo
7\fI\,grep {pattern | -e pattern} \/\fR[\fI\,<project>\/\fR...]
8.SH DESCRIPTION
9Summary
10.PP
11Print lines matching a pattern
12.SH OPTIONS
13.TP
14\fB\-h\fR, \fB\-\-help\fR
15show this help message and exit
16.TP
17\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
18number of jobs to run in parallel (default: based on
19number of CPU cores)
20.SS Logging options:
21.TP
22\fB\-\-verbose\fR
23show all output
24.TP
25\fB\-q\fR, \fB\-\-quiet\fR
26only show errors
27.SS Sources:
28.TP
29\fB\-\-cached\fR
30Search the index, instead of the work tree
31.TP
32\fB\-r\fR TREEish, \fB\-\-revision\fR=\fI\,TREEish\/\fR
33Search TREEish, instead of the work tree
34.SS Pattern:
35.TP
36\fB\-e\fR PATTERN
37Pattern to search for
38.TP
39\fB\-i\fR, \fB\-\-ignore\-case\fR
40Ignore case differences
41.TP
42\fB\-a\fR, \fB\-\-text\fR
43Process binary files as if they were text
44.TP
45\fB\-I\fR
46Don't match the pattern in binary files
47.TP
48\fB\-w\fR, \fB\-\-word\-regexp\fR
49Match the pattern only at word boundaries
50.TP
51\fB\-v\fR, \fB\-\-invert\-match\fR
52Select non\-matching lines
53.TP
54\fB\-G\fR, \fB\-\-basic\-regexp\fR
55Use POSIX basic regexp for patterns (default)
56.TP
57\fB\-E\fR, \fB\-\-extended\-regexp\fR
58Use POSIX extended regexp for patterns
59.TP
60\fB\-F\fR, \fB\-\-fixed\-strings\fR
61Use fixed strings (not regexp) for pattern
62.SS Pattern Grouping:
63.TP
64\fB\-\-all\-match\fR
65Limit match to lines that have all patterns
66.TP
67\fB\-\-and\fR, \fB\-\-or\fR, \fB\-\-not\fR
68Boolean operators to combine patterns
69.TP
70\-(, \-)
71Boolean operator grouping
72.SS Output:
73.TP
74\fB\-n\fR
75Prefix the line number to matching lines
76.TP
77\fB\-C\fR CONTEXT
78Show CONTEXT lines around match
79.TP
80\fB\-B\fR CONTEXT
81Show CONTEXT lines before match
82.TP
83\fB\-A\fR CONTEXT
84Show CONTEXT lines after match
85.TP
86\fB\-l\fR, \fB\-\-name\-only\fR, \fB\-\-files\-with\-matches\fR
87Show only file names containing matching lines
88.TP
89\fB\-L\fR, \fB\-\-files\-without\-match\fR
90Show only file names not containing matching lines
91.PP
92Run `repo help grep` to view the detailed manual.
93.SH DETAILS
94.PP
95Search for the specified patterns in all project files.
96.PP
97Boolean Options
98.PP
99The following options can appear as often as necessary to express the pattern to
100locate:
101.HP
102\fB\-e\fR PATTERN
103.HP
104\fB\-\-and\fR, \fB\-\-or\fR, \fB\-\-not\fR, \-(, \-)
105.PP
106Further, the \fB\-r\fR/\-\-revision option may be specified multiple times in order to
107scan multiple trees. If the same file matches in more than one tree, only the
108first result is reported, prefixed by the revision name it was found under.
109.PP
110Examples
111.PP
112Look for a line that has '#define' and either 'MAX_PATH or 'PATH_MAX':
113.IP
114repo grep \fB\-e\fR '#define' \fB\-\-and\fR \-\e( \fB\-e\fR MAX_PATH \fB\-e\fR PATH_MAX \e)
115.PP
116Look for a line that has 'NODE' or 'Unexpected' in files that contain a line
117that matches both expressions:
118.IP
119repo grep \fB\-\-all\-match\fR \fB\-e\fR NODE \fB\-e\fR Unexpected
diff --git a/man/repo-help.1 b/man/repo-help.1
new file mode 100644
index 00000000..d6da3c51
--- /dev/null
+++ b/man/repo-help.1
@@ -0,0 +1,33 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo help" "Repo Manual"
3.SH NAME
4repo \- repo help - manual page for repo help
5.SH SYNOPSIS
6.B repo
7\fI\,help \/\fR[\fI\,--all|command\/\fR]
8.SH DESCRIPTION
9Summary
10.PP
11Display detailed help on a command
12.SH OPTIONS
13.TP
14\fB\-h\fR, \fB\-\-help\fR
15show this help message and exit
16.TP
17\fB\-a\fR, \fB\-\-all\fR
18show the complete list of commands
19.TP
20\fB\-\-help\-all\fR
21show the \fB\-\-help\fR of all commands
22.SS Logging options:
23.TP
24\fB\-v\fR, \fB\-\-verbose\fR
25show all output
26.TP
27\fB\-q\fR, \fB\-\-quiet\fR
28only show errors
29.PP
30Run `repo help help` to view the detailed manual.
31.SH DETAILS
32.PP
33Displays detailed usage information about a command.
diff --git a/man/repo-info.1 b/man/repo-info.1
new file mode 100644
index 00000000..cf7c17b8
--- /dev/null
+++ b/man/repo-info.1
@@ -0,0 +1,40 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo info" "Repo Manual"
3.SH NAME
4repo \- 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\,<project>\/\fR...]
8.SH DESCRIPTION
9Summary
10.PP
11Get info on the manifest branch, current branch or unmerged branches
12.SH OPTIONS
13.TP
14\fB\-h\fR, \fB\-\-help\fR
15show this help message and exit
16.TP
17\fB\-d\fR, \fB\-\-diff\fR
18show full info and commit diff including remote
19branches
20.TP
21\fB\-o\fR, \fB\-\-overview\fR
22show overview of all local commits
23.TP
24\fB\-c\fR, \fB\-\-current\-branch\fR
25consider only checked out branches
26.TP
27\fB\-\-no\-current\-branch\fR
28consider all local branches
29.TP
30\fB\-l\fR, \fB\-\-local\-only\fR
31disable all remote operations
32.SS Logging options:
33.TP
34\fB\-v\fR, \fB\-\-verbose\fR
35show all output
36.TP
37\fB\-q\fR, \fB\-\-quiet\fR
38only show errors
39.PP
40Run `repo help info` to view the detailed manual.
diff --git a/man/repo-init.1 b/man/repo-init.1
new file mode 100644
index 00000000..9957b64d
--- /dev/null
+++ b/man/repo-init.1
@@ -0,0 +1,170 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "September 2021" "repo init" "Repo Manual"
3.SH NAME
4repo \- repo init - manual page for repo init
5.SH SYNOPSIS
6.B repo
7\fI\,init \/\fR[\fI\,options\/\fR] [\fI\,manifest url\/\fR]
8.SH DESCRIPTION
9Summary
10.PP
11Initialize a repo client checkout in the current directory
12.SH OPTIONS
13.TP
14\fB\-h\fR, \fB\-\-help\fR
15show this help message and exit
16.SS Logging options:
17.TP
18\fB\-v\fR, \fB\-\-verbose\fR
19show all output
20.TP
21\fB\-q\fR, \fB\-\-quiet\fR
22only show errors
23.SS Manifest options:
24.TP
25\fB\-u\fR URL, \fB\-\-manifest\-url\fR=\fI\,URL\/\fR
26manifest repository location
27.TP
28\fB\-b\fR REVISION, \fB\-\-manifest\-branch\fR=\fI\,REVISION\/\fR
29manifest branch or revision (use HEAD for default)
30.TP
31\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
32initial manifest file
33.TP
34\fB\-\-standalone\-manifest\fR
35download the manifest as a static file rather then
36create a git checkout of the manifest repo
37.TP
38\fB\-g\fR GROUP, \fB\-\-groups\fR=\fI\,GROUP\/\fR
39restrict manifest projects to ones with specified
40group(s) [default|all|G1,G2,G3|G4,\-G5,\-G6]
41.TP
42\fB\-p\fR PLATFORM, \fB\-\-platform\fR=\fI\,PLATFORM\/\fR
43restrict manifest projects to ones with a specified
44platform group [auto|all|none|linux|darwin|...]
45.TP
46\fB\-\-submodules\fR
47sync any submodules associated with the manifest repo
48.SS Manifest (only) checkout options:
49.TP
50\fB\-c\fR, \fB\-\-current\-branch\fR
51fetch only current manifest branch from server
52.TP
53\fB\-\-no\-current\-branch\fR
54fetch all manifest branches from server
55.TP
56\fB\-\-tags\fR
57fetch tags in the manifest
58.TP
59\fB\-\-no\-tags\fR
60don't fetch tags in the manifest
61.SS Checkout modes:
62.TP
63\fB\-\-mirror\fR
64create a replica of the remote repositories rather
65than a client working directory
66.TP
67\fB\-\-archive\fR
68checkout an archive instead of a git repository for
69each project. See git archive.
70.TP
71\fB\-\-worktree\fR
72use git\-worktree to manage projects
73.SS Project checkout optimizations:
74.TP
75\fB\-\-reference\fR=\fI\,DIR\/\fR
76location of mirror directory
77.TP
78\fB\-\-dissociate\fR
79dissociate from reference mirrors after clone
80.TP
81\fB\-\-depth\fR=\fI\,DEPTH\/\fR
82create a shallow clone with given depth; see git clone
83.TP
84\fB\-\-partial\-clone\fR
85perform partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code)
86.TP
87\fB\-\-no\-partial\-clone\fR
88disable use of partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code)
89.TP
90\fB\-\-partial\-clone\-exclude\fR=\fI\,PARTIAL_CLONE_EXCLUDE\/\fR
91exclude the specified projects (a comma\-delimited
92project names) from partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code)
93.TP
94\fB\-\-clone\-filter\fR=\fI\,CLONE_FILTER\/\fR
95filter for use with \fB\-\-partial\-clone\fR [default:
96blob:none]
97.TP
98\fB\-\-use\-superproject\fR
99use the manifest superproject to sync projects
100.TP
101\fB\-\-no\-use\-superproject\fR
102disable use of manifest superprojects
103.TP
104\fB\-\-clone\-bundle\fR
105enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS (default if
106not \fB\-\-partial\-clone\fR)
107.TP
108\fB\-\-no\-clone\-bundle\fR
109disable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS (default if
110\fB\-\-partial\-clone\fR)
111.SS repo Version options:
112.TP
113\fB\-\-repo\-url\fR=\fI\,URL\/\fR
114repo repository location ($REPO_URL)
115.TP
116\fB\-\-repo\-rev\fR=\fI\,REV\/\fR
117repo branch or revision ($REPO_REV)
118.TP
119\fB\-\-no\-repo\-verify\fR
120do not verify repo source code
121.SS Other options:
122.TP
123\fB\-\-config\-name\fR
124Always prompt for name/e\-mail
125.PP
126Run `repo help init` to view the detailed manual.
127.SH DETAILS
128.PP
129The 'repo init' command is run once to install and initialize repo. The latest
130repo source code and manifest collection is downloaded from the server and is
131installed in the .repo/ directory in the current working directory.
132.PP
133When creating a new checkout, the manifest URL is the only required setting. It
134may be specified using the \fB\-\-manifest\-url\fR option, or as the first optional
135argument.
136.PP
137The optional \fB\-b\fR argument can be used to select the manifest branch to checkout
138and use. If no branch is specified, the remote's default branch is used. This is
139equivalent to using \fB\-b\fR HEAD.
140.PP
141The optional \fB\-m\fR argument can be used to specify an alternate manifest to be
142used. If no manifest is specified, the manifest default.xml will be used.
143.PP
144If the \fB\-\-standalone\-manifest\fR argument is set, the manifest will be downloaded
145directly from the specified \fB\-\-manifest\-url\fR as a static file (rather than setting
146up a manifest git checkout). With \fB\-\-standalone\-manifest\fR, the manifest will be
147fully static and will not be re\-downloaded during subsesquent `repo init` and
148`repo sync` calls.
149.PP
150The \fB\-\-reference\fR option can be used to point to a directory that has the content
151of a \fB\-\-mirror\fR sync. This will make the working directory use as much data as
152possible from the local reference directory when fetching from the server. This
153will make the sync go a lot faster by reducing data traffic on the network.
154.PP
155The \fB\-\-dissociate\fR option can be used to borrow the objects from the directory
156specified with the \fB\-\-reference\fR option only to reduce network transfer, and stop
157borrowing from them after a first clone is made by making necessary local copies
158of borrowed objects.
159.PP
160The \fB\-\-no\-clone\-bundle\fR option disables any attempt to use \fI\,$URL/clone.bundle\/\fP to
161bootstrap a new Git repository from a resumeable bundle file on a content
162delivery network. This may be necessary if there are problems with the local
163Python HTTP client or proxy configuration, but the Git binary works.
164.PP
165Switching Manifest Branches
166.PP
167To switch to another manifest branch, `repo init \fB\-b\fR otherbranch` may be used in
168an existing client. However, as this only updates the manifest, a subsequent
169`repo sync` (or `repo sync \fB\-d\fR`) is necessary to update the working directory
170files.
diff --git a/man/repo-list.1 b/man/repo-list.1
new file mode 100644
index 00000000..7f85e612
--- /dev/null
+++ b/man/repo-list.1
@@ -0,0 +1,61 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo list" "Repo Manual"
3.SH NAME
4repo \- repo list - manual page for repo list
5.SH SYNOPSIS
6.B repo
7\fI\,list \/\fR[\fI\,-f\/\fR] [\fI\,<project>\/\fR...]
8.SH DESCRIPTION
9Summary
10.PP
11List projects and their associated directories
12.PP
13repo list [\-f] \fB\-r\fR str1 [str2]...
14.SH OPTIONS
15.TP
16\fB\-h\fR, \fB\-\-help\fR
17show this help message and exit
18.TP
19\fB\-r\fR, \fB\-\-regex\fR
20filter the project list based on regex or wildcard
21matching of strings
22.TP
23\fB\-g\fR GROUPS, \fB\-\-groups\fR=\fI\,GROUPS\/\fR
24filter the project list based on the groups the
25project is in
26.TP
27\fB\-a\fR, \fB\-\-all\fR
28show projects regardless of checkout state
29.TP
30\fB\-n\fR, \fB\-\-name\-only\fR
31display only the name of the repository
32.TP
33\fB\-p\fR, \fB\-\-path\-only\fR
34display only the path of the repository
35.TP
36\fB\-f\fR, \fB\-\-fullpath\fR
37display the full work tree path instead of the
38relative path
39.TP
40\fB\-\-relative\-to\fR=\fI\,PATH\/\fR
41display paths relative to this one (default: top of
42repo client checkout)
43.SS Logging options:
44.TP
45\fB\-v\fR, \fB\-\-verbose\fR
46show all output
47.TP
48\fB\-q\fR, \fB\-\-quiet\fR
49only show errors
50.PP
51Run `repo help list` to view the detailed manual.
52.SH DETAILS
53.PP
54List all projects; pass '.' to list the project for the cwd.
55.PP
56By default, only projects that currently exist in the checkout are shown. If you
57want to list all projects (using the specified filter settings), use the \fB\-\-all\fR
58option. If you want to show all projects regardless of the manifest groups, then
59also pass \fB\-\-groups\fR all.
60.PP
61This is similar to running: repo forall \fB\-c\fR 'echo "$REPO_PATH : $REPO_PROJECT"'.
diff --git a/man/repo-manifest.1 b/man/repo-manifest.1
new file mode 100644
index 00000000..be467607
--- /dev/null
+++ b/man/repo-manifest.1
@@ -0,0 +1,548 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo manifest" "Repo Manual"
3.SH NAME
4repo \- repo manifest - manual page for repo manifest
5.SH SYNOPSIS
6.B repo
7\fI\,manifest \/\fR[\fI\,-o {-|NAME.xml}\/\fR] [\fI\,-m MANIFEST.xml\/\fR] [\fI\,-r\/\fR]
8.SH DESCRIPTION
9Summary
10.PP
11Manifest inspection utility
12.SH OPTIONS
13.TP
14\fB\-h\fR, \fB\-\-help\fR
15show this help message and exit
16.TP
17\fB\-r\fR, \fB\-\-revision\-as\-HEAD\fR
18save revisions as current HEAD
19.TP
20\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
21temporary manifest to use for this sync
22.TP
23\fB\-\-suppress\-upstream\-revision\fR
24if in \fB\-r\fR mode, do not write the upstream field (only
25of use if the branch names for a sha1 manifest are
26sensitive)
27.TP
28\fB\-\-suppress\-dest\-branch\fR
29if in \fB\-r\fR mode, do not write the dest\-branch field
30(only of use if the branch names for a sha1 manifest
31are sensitive)
32.TP
33\fB\-\-json\fR
34output manifest in JSON format (experimental)
35.TP
36\fB\-\-pretty\fR
37format output for humans to read
38.TP
39\fB\-\-no\-local\-manifests\fR
40ignore local manifests
41.TP
42\fB\-o\fR \-|NAME.xml, \fB\-\-output\-file\fR=\fI\,\-\/\fR|NAME.xml
43file to save the manifest to
44.SS Logging options:
45.TP
46\fB\-v\fR, \fB\-\-verbose\fR
47show all output
48.TP
49\fB\-q\fR, \fB\-\-quiet\fR
50only show errors
51.PP
52Run `repo help manifest` to view the detailed manual.
53.SH DETAILS
54.PP
55With the \fB\-o\fR option, exports the current manifest for inspection. The manifest
56and (if present) local_manifests/ are combined together to produce a single
57manifest file. This file can be stored in a Git repository for use during future
58\&'repo init' invocations.
59.PP
60The \fB\-r\fR option can be used to generate a manifest file with project revisions set
61to the current commit hash. These are known as "revision locked manifests", as
62they don't follow a particular branch. In this case, the 'upstream' attribute is
63set to the ref we were on when the manifest was generated. The 'dest\-branch'
64attribute is set to indicate the remote ref to push changes to via 'repo
65upload'.
66.PP
67repo Manifest Format
68.PP
69A repo manifest describes the structure of a repo client; that is the
70directories that are visible and where they should be obtained from with git.
71.PP
72The basic structure of a manifest is a bare Git repository holding a single
73`default.xml` XML file in the top level directory.
74.PP
75Manifests are inherently version controlled, since they are kept within a Git
76repository. Updates to manifests are automatically obtained by clients during
77`repo sync`.
78.PP
79[TOC]
80.PP
81XML File Format
82.PP
83A manifest XML file (e.g. `default.xml`) roughly conforms to the following DTD:
84.PP
85```xml <!DOCTYPE manifest [
86.TP
87<!ELEMENT manifest (notice?,
88remote*,
89default?,
90manifest\-server?,
91remove\-project*,
92project*,
93extend\-project*,
94repo\-hooks?,
95superproject?,
96contactinfo?,
97include*)>
98.IP
99<!ELEMENT notice (#PCDATA)>
100.IP
101<!ELEMENT remote (annotation*)>
102<!ATTLIST remote name ID #REQUIRED>
103<!ATTLIST remote alias CDATA #IMPLIED>
104<!ATTLIST remote fetch CDATA #REQUIRED>
105<!ATTLIST remote pushurl CDATA #IMPLIED>
106<!ATTLIST remote review CDATA #IMPLIED>
107<!ATTLIST remote revision CDATA #IMPLIED>
108.IP
109<!ELEMENT default EMPTY>
110<!ATTLIST default remote IDREF #IMPLIED>
111<!ATTLIST default revision CDATA #IMPLIED>
112<!ATTLIST default dest\-branch CDATA #IMPLIED>
113<!ATTLIST default upstream CDATA #IMPLIED>
114<!ATTLIST default sync\-j CDATA #IMPLIED>
115<!ATTLIST default sync\-c CDATA #IMPLIED>
116<!ATTLIST default sync\-s CDATA #IMPLIED>
117<!ATTLIST default sync\-tags CDATA #IMPLIED>
118.IP
119<!ELEMENT manifest\-server EMPTY>
120<!ATTLIST manifest\-server url CDATA #REQUIRED>
121.TP
122<!ELEMENT project (annotation*,
123project*,
124copyfile*,
125linkfile*)>
126.TP
127<!ATTLIST project name
128CDATA #REQUIRED>
129.TP
130<!ATTLIST project path
131CDATA #IMPLIED>
132.TP
133<!ATTLIST project remote
134IDREF #IMPLIED>
135.TP
136<!ATTLIST project revision
137CDATA #IMPLIED>
138.IP
139<!ATTLIST project dest\-branch CDATA #IMPLIED>
140<!ATTLIST project groups CDATA #IMPLIED>
141<!ATTLIST project sync\-c CDATA #IMPLIED>
142<!ATTLIST project sync\-s CDATA #IMPLIED>
143<!ATTLIST project sync\-tags CDATA #IMPLIED>
144<!ATTLIST project upstream CDATA #IMPLIED>
145<!ATTLIST project clone\-depth CDATA #IMPLIED>
146<!ATTLIST project force\-path CDATA #IMPLIED>
147.IP
148<!ELEMENT annotation EMPTY>
149<!ATTLIST annotation name CDATA #REQUIRED>
150<!ATTLIST annotation value CDATA #REQUIRED>
151<!ATTLIST annotation keep CDATA "true">
152.IP
153<!ELEMENT copyfile EMPTY>
154<!ATTLIST copyfile src CDATA #REQUIRED>
155<!ATTLIST copyfile dest CDATA #REQUIRED>
156.IP
157<!ELEMENT linkfile EMPTY>
158<!ATTLIST linkfile src CDATA #REQUIRED>
159<!ATTLIST linkfile dest CDATA #REQUIRED>
160.IP
161<!ELEMENT extend\-project EMPTY>
162<!ATTLIST extend\-project name CDATA #REQUIRED>
163<!ATTLIST extend\-project path CDATA #IMPLIED>
164<!ATTLIST extend\-project groups CDATA #IMPLIED>
165<!ATTLIST extend\-project revision CDATA #IMPLIED>
166<!ATTLIST extend\-project remote CDATA #IMPLIED>
167.IP
168<!ELEMENT remove\-project EMPTY>
169<!ATTLIST remove\-project name CDATA #REQUIRED>
170<!ATTLIST remove\-project optional CDATA #IMPLIED>
171.IP
172<!ELEMENT repo\-hooks EMPTY>
173<!ATTLIST repo\-hooks in\-project CDATA #REQUIRED>
174<!ATTLIST repo\-hooks enabled\-list CDATA #REQUIRED>
175.IP
176<!ELEMENT superproject EMPTY>
177<!ATTLIST superproject name CDATA #REQUIRED>
178<!ATTLIST superproject remote IDREF #IMPLIED>
179.IP
180<!ELEMENT contactinfo EMPTY>
181<!ATTLIST contactinfo bugurl CDATA #REQUIRED>
182.IP
183<!ELEMENT include EMPTY>
184<!ATTLIST include name CDATA #REQUIRED>
185<!ATTLIST include groups CDATA #IMPLIED>
186.PP
187]>
188```
189.PP
190For compatibility purposes across repo releases, all unknown elements are
191silently ignored. However, repo reserves all possible names for itself for
192future use. If you want to use custom elements, the `x\-*` namespace is reserved
193for that purpose, and repo guarantees to never allocate any corresponding names.
194.PP
195A description of the elements and their attributes follows.
196.PP
197Element manifest
198.PP
199The root element of the file.
200.PP
201Element notice
202.PP
203Arbitrary text that is displayed to users whenever `repo sync` finishes. The
204content is simply passed through as it exists in the manifest.
205.PP
206Element remote
207.PP
208One or more remote elements may be specified. Each remote element specifies a
209Git URL shared by one or more projects and (optionally) the Gerrit review server
210those projects upload changes through.
211.PP
212Attribute `name`: A short name unique to this manifest file. The name specified
213here is used as the remote name in each project's .git/config, and is therefore
214automatically available to commands like `git fetch`, `git remote`, `git pull`
215and `git push`.
216.PP
217Attribute `alias`: The alias, if specified, is used to override `name` to be set
218as the remote name in each project's .git/config. Its value can be duplicated
219while attribute `name` has to be unique in the manifest file. This helps each
220project to be able to have same remote name which actually points to different
221remote url.
222.PP
223Attribute `fetch`: The Git URL prefix for all projects which use this remote.
224Each project's name is appended to this prefix to form the actual URL used to
225clone the project.
226.PP
227Attribute `pushurl`: The Git "push" URL prefix for all projects which use this
228remote. Each project's name is appended to this prefix to form the actual URL
229used to "git push" the project. This attribute is optional; if not specified
230then "git push" will use the same URL as the `fetch` attribute.
231.PP
232Attribute `review`: Hostname of the Gerrit server where reviews are uploaded to
233by `repo upload`. This attribute is optional; if not specified then `repo
234upload` will not function.
235.PP
236Attribute `revision`: Name of a Git branch (e.g. `main` or `refs/heads/main`).
237Remotes with their own revision will override the default revision.
238.PP
239Element default
240.PP
241At most one default element may be specified. Its remote and revision attributes
242are used when a project element does not specify its own remote or revision
243attribute.
244.PP
245Attribute `remote`: Name of a previously defined remote element. Project
246elements lacking a remote attribute of their own will use this remote.
247.PP
248Attribute `revision`: Name of a Git branch (e.g. `main` or `refs/heads/main`).
249Project elements lacking their own revision attribute will use this revision.
250.PP
251Attribute `dest\-branch`: Name of a Git branch (e.g. `main`). Project elements
252not setting their own `dest\-branch` will inherit this value. If this value is
253not set, projects will use `revision` by default instead.
254.PP
255Attribute `upstream`: Name of the Git ref in which a sha1 can be found. Used
256when syncing a revision locked manifest in \fB\-c\fR mode to avoid having to sync the
257entire ref space. Project elements not setting their own `upstream` will inherit
258this value.
259.PP
260Attribute `sync\-j`: Number of parallel jobs to use when synching.
261.PP
262Attribute `sync\-c`: Set to true to only sync the given Git branch (specified in
263the `revision` attribute) rather than the whole ref space. Project elements
264lacking a sync\-c element of their own will use this value.
265.PP
266Attribute `sync\-s`: Set to true to also sync sub\-projects.
267.PP
268Attribute `sync\-tags`: Set to false to only sync the given Git branch (specified
269in the `revision` attribute) rather than the other ref tags.
270.PP
271Element manifest\-server
272.PP
273At most one manifest\-server may be specified. The url attribute is used to
274specify the URL of a manifest server, which is an XML RPC service.
275.PP
276The manifest server should implement the following RPC methods:
277.IP
278GetApprovedManifest(branch, target)
279.PP
280Return a manifest in which each project is pegged to a known good revision for
281the current branch and target. This is used by repo sync when the \fB\-\-smart\-sync\fR
282option is given.
283.PP
284The target to use is defined by environment variables TARGET_PRODUCT and
285TARGET_BUILD_VARIANT. These variables are used to create a string of the form
286$TARGET_PRODUCT\-$TARGET_BUILD_VARIANT, e.g. passion\-userdebug. If one of those
287variables or both are not present, the program will call GetApprovedManifest
288without the target parameter and the manifest server should choose a reasonable
289default target.
290.IP
291GetManifest(tag)
292.PP
293Return a manifest in which each project is pegged to the revision at the
294specified tag. This is used by repo sync when the \fB\-\-smart\-tag\fR option is given.
295.PP
296Element project
297.PP
298One or more project elements may be specified. Each element describes a single
299Git repository to be cloned into the repo client workspace. You may specify
300Git\-submodules by creating a nested project. Git\-submodules will be
301automatically recognized and inherit their parent's attributes, but those may be
302overridden by an explicitly specified project element.
303.PP
304Attribute `name`: A unique name for this project. The project's name is appended
305onto its remote's fetch URL to generate the actual URL to configure the Git
306remote with. The URL gets formed as:
307.IP
308${remote_fetch}/${project_name}.git
309.PP
310where ${remote_fetch} is the remote's fetch attribute and ${project_name} is the
311project's name attribute. The suffix ".git" is always appended as repo assumes
312the upstream is a forest of bare Git repositories. If the project has a parent
313element, its name will be prefixed by the parent's.
314.PP
315The project name must match the name Gerrit knows, if Gerrit is being used for
316code reviews.
317.PP
318"name" must not be empty, and may not be an absolute path or use "." or ".."
319path components. It is always interpreted relative to the remote's fetch
320settings, so if a different base path is needed, declare a different remote with
321the new settings needed. These restrictions are not enforced for [Local
322Manifests].
323.PP
324Attribute `path`: An optional path relative to the top directory of the repo
325client where the Git working directory for this project should be placed. If not
326supplied the project "name" is used. If the project has a parent element, its
327path will be prefixed by the parent's.
328.PP
329"path" may not be an absolute path or use "." or ".." path components. These
330restrictions are not enforced for [Local Manifests].
331.PP
332If you want to place files into the root of the checkout (e.g. a README or
333Makefile or another build script), use the [copyfile] or [linkfile] elements
334instead.
335.PP
336Attribute `remote`: Name of a previously defined remote element. If not supplied
337the remote given by the default element is used.
338.PP
339Attribute `revision`: Name of the Git branch the manifest wants to track for
340this project. Names can be relative to refs/heads (e.g. just "main") or absolute
341(e.g. "refs/heads/main"). Tags and/or explicit SHA\-1s should work in theory, but
342have not been extensively tested. If not supplied the revision given by the
343remote element is used if applicable, else the default element is used.
344.PP
345Attribute `dest\-branch`: Name of a Git branch (e.g. `main`). When using `repo
346upload`, changes will be submitted for code review on this branch. If
347unspecified both here and in the default element, `revision` is used instead.
348.PP
349Attribute `groups`: List of groups to which this project belongs, whitespace or
350comma separated. All projects belong to the group "all", and each project
351automatically belongs to a group of its name:`name` and path:`path`. E.g. for
352`<project name="monkeys" path="barrel\-of"/>`, that project definition is
353implicitly in the following manifest groups: default, name:monkeys, and
354path:barrel\-of. If you place a project in the group "notdefault", it will not be
355automatically downloaded by repo. If the project has a parent element, the
356`name` and `path` here are the prefixed ones.
357.PP
358Attribute `sync\-c`: Set to true to only sync the given Git branch (specified in
359the `revision` attribute) rather than the whole ref space.
360.PP
361Attribute `sync\-s`: Set to true to also sync sub\-projects.
362.PP
363Attribute `upstream`: Name of the Git ref in which a sha1 can be found. Used
364when syncing a revision locked manifest in \fB\-c\fR mode to avoid having to sync the
365entire ref space.
366.PP
367Attribute `clone\-depth`: Set the depth to use when fetching this project. If
368specified, this value will override any value given to repo init with the
369\fB\-\-depth\fR option on the command line.
370.PP
371Attribute `force\-path`: Set to true to force this project to create the local
372mirror repository according to its `path` attribute (if supplied) rather than
373the `name` attribute. This attribute only applies to the local mirrors syncing,
374it will be ignored when syncing the projects in a client working directory.
375.PP
376Element extend\-project
377.PP
378Modify the attributes of the named project.
379.PP
380This element is mostly useful in a local manifest file, to modify the attributes
381of an existing project without completely replacing the existing project
382definition. This makes the local manifest more robust against changes to the
383original manifest.
384.PP
385Attribute `path`: If specified, limit the change to projects checked out at the
386specified path, rather than all projects with the given name.
387.PP
388Attribute `groups`: List of additional groups to which this project belongs.
389Same syntax as the corresponding element of `project`.
390.PP
391Attribute `revision`: If specified, overrides the revision of the original
392project. Same syntax as the corresponding element of `project`.
393.PP
394Attribute `remote`: If specified, overrides the remote of the original project.
395Same syntax as the corresponding element of `project`.
396.PP
397Element annotation
398.PP
399Zero or more annotation elements may be specified as children of a project or
400remote element. Each element describes a name\-value pair. For projects, this
401name\-value pair will be exported into each project's environment during a
402\&'forall' command, prefixed with `REPO__`. In addition, there is an optional
403attribute "keep" which accepts the case insensitive values "true" (default) or
404"false". This attribute determines whether or not the annotation will be kept
405when exported with the manifest subcommand.
406.PP
407Element copyfile
408.PP
409Zero or more copyfile elements may be specified as children of a project
410element. Each element describes a src\-dest pair of files; the "src" file will be
411copied to the "dest" place during `repo sync` command.
412.PP
413"src" is project relative, "dest" is relative to the top of the tree. Copying
414from paths outside of the project or to paths outside of the repo client is not
415allowed.
416.PP
417"src" and "dest" must be files. Directories or symlinks are not allowed.
418Intermediate paths must not be symlinks either.
419.PP
420Parent directories of "dest" will be automatically created if missing.
421.PP
422Element linkfile
423.PP
424It's just like copyfile and runs at the same time as copyfile but instead of
425copying it creates a symlink.
426.PP
427The symlink is created at "dest" (relative to the top of the tree) and points to
428the path specified by "src" which is a path in the project.
429.PP
430Parent directories of "dest" will be automatically created if missing.
431.PP
432The symlink target may be a file or directory, but it may not point outside of
433the repo client.
434.PP
435Element remove\-project
436.PP
437Deletes the named project from the internal manifest table, possibly allowing a
438subsequent project element in the same manifest file to replace the project with
439a different source.
440.PP
441This element is mostly useful in a local manifest file, where the user can
442remove a project, and possibly replace it with their own definition.
443.PP
444Attribute `optional`: Set to true to ignore remove\-project elements with no
445matching `project` element.
446.PP
447Element repo\-hooks
448.PP
449NB: See the [practical documentation](./repo\-hooks.md) for using repo hooks.
450.PP
451Only one repo\-hooks element may be specified at a time. Attempting to redefine
452it will fail to parse.
453.PP
454Attribute `in\-project`: The project where the hooks are defined. The value must
455match the `name` attribute (**not** the `path` attribute) of a previously
456defined `project` element.
457.PP
458Attribute `enabled\-list`: List of hooks to use, whitespace or comma separated.
459.PP
460Element superproject
461.PP
462*** *Note*: This is currently a WIP. ***
463.PP
464NB: See the [git superprojects documentation](
465https://en.wikibooks.org/wiki/Git/Submodules_and_Superprojects) for background
466information.
467.PP
468This element is used to specify the URL of the superproject. It has "name" and
469"remote" as atrributes. Only "name" is required while the others have reasonable
470defaults. At most one superproject may be specified. Attempting to redefine it
471will fail to parse.
472.PP
473Attribute `name`: A unique name for the superproject. This attribute has the
474same meaning as project's name attribute. See the [element
475project](#element\-project) for more information.
476.PP
477Attribute `remote`: Name of a previously defined remote element. If not supplied
478the remote given by the default element is used.
479.PP
480Element contactinfo
481.PP
482*** *Note*: This is currently a WIP. ***
483.PP
484This element is used to let manifest authors self\-register contact info. It has
485"bugurl" as a required atrribute. This element can be repeated, and any later
486entries will clobber earlier ones. This would allow manifest authors who extend
487manifests to specify their own contact info.
488.PP
489Attribute `bugurl`: The URL to file a bug against the manifest owner.
490.PP
491Element include
492.PP
493This element provides the capability of including another manifest file into the
494originating manifest. Normal rules apply for the target manifest to include \- it
495must be a usable manifest on its own.
496.PP
497Attribute `name`: the manifest to include, specified relative to the manifest
498repository's root.
499.PP
500"name" may not be an absolute path or use "." or ".." path components. These
501restrictions are not enforced for [Local Manifests].
502.PP
503Attribute `groups`: List of additional groups to which all projects in the
504included manifest belong. This appends and recurses, meaning all projects in
505sub\-manifests carry all parent include groups. Same syntax as the corresponding
506element of `project`.
507.PP
508Local Manifests
509.PP
510Additional remotes and projects may be added through local manifest files stored
511in `$TOP_DIR/.repo/local_manifests/*.xml`.
512.PP
513For example:
514.IP
515\f(CW$ ls .repo/local_manifests\fR
516.IP
517local_manifest.xml
518another_local_manifest.xml
519.IP
520\f(CW$ cat .repo/local_manifests/local_manifest.xml\fR
521.IP
522<?xml version="1.0" encoding="UTF\-8"?>
523<manifest>
524.IP
525<project path="manifest"
526.IP
527name="tools/manifest" />
528.IP
529<project path="platform\-manifest"
530.IP
531name="platform/manifest" />
532.IP
533</manifest>
534.PP
535Users may add projects to the local manifest(s) prior to a `repo sync`
536invocation, instructing repo to automatically download and manage these extra
537projects.
538.PP
539Manifest files stored in `$TOP_DIR/.repo/local_manifests/*.xml` will be loaded
540in alphabetical order.
541.PP
542Projects from local manifest files are added into local::<local manifest
543filename> group.
544.PP
545The legacy `$TOP_DIR/.repo/local_manifest.xml` path is no longer supported.
546.SS [copyfile]: #Element\-copyfile [linkfile]: #Element\-linkfile [Local Manifests]:
547.PP
548#local\-manifests
diff --git a/man/repo-overview.1 b/man/repo-overview.1
new file mode 100644
index 00000000..a12c7640
--- /dev/null
+++ b/man/repo-overview.1
@@ -0,0 +1,39 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo overview" "Repo Manual"
3.SH NAME
4repo \- repo overview - manual page for repo overview
5.SH SYNOPSIS
6.B repo
7\fI\,overview \/\fR[\fI\,--current-branch\/\fR] [\fI\,<project>\/\fR...]
8.SH DESCRIPTION
9Summary
10.PP
11Display overview of unmerged project branches
12.SH OPTIONS
13.TP
14\fB\-h\fR, \fB\-\-help\fR
15show this help message and exit
16.TP
17\fB\-c\fR, \fB\-\-current\-branch\fR
18consider only checked out branches
19.TP
20\fB\-\-no\-current\-branch\fR
21consider all local branches
22.SS Logging options:
23.TP
24\fB\-v\fR, \fB\-\-verbose\fR
25show all output
26.TP
27\fB\-q\fR, \fB\-\-quiet\fR
28only show errors
29.PP
30Run `repo help overview` to view the detailed manual.
31.SH DETAILS
32.PP
33The 'repo overview' command is used to display an overview of the projects
34branches, and list any local commits that have not yet been merged into the
35project.
36.PP
37The \fB\-c\fR/\-\-current\-branch option can be used to restrict the output to only
38branches currently checked out in each project. By default, all branches are
39displayed.
diff --git a/man/repo-prune.1 b/man/repo-prune.1
new file mode 100644
index 00000000..bd68a373
--- /dev/null
+++ b/man/repo-prune.1
@@ -0,0 +1,28 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo prune" "Repo Manual"
3.SH NAME
4repo \- repo prune - manual page for repo prune
5.SH SYNOPSIS
6.B repo
7\fI\,prune \/\fR[\fI\,<project>\/\fR...]
8.SH DESCRIPTION
9Summary
10.PP
11Prune (delete) already merged topics
12.SH OPTIONS
13.TP
14\fB\-h\fR, \fB\-\-help\fR
15show this help message and exit
16.TP
17\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
18number of jobs to run in parallel (default: based on
19number of CPU cores)
20.SS Logging options:
21.TP
22\fB\-v\fR, \fB\-\-verbose\fR
23show all output
24.TP
25\fB\-q\fR, \fB\-\-quiet\fR
26only show errors
27.PP
28Run `repo help prune` to view the detailed manual.
diff --git a/man/repo-rebase.1 b/man/repo-rebase.1
new file mode 100644
index 00000000..aa261036
--- /dev/null
+++ b/man/repo-rebase.1
@@ -0,0 +1,55 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo rebase" "Repo Manual"
3.SH NAME
4repo \- repo rebase - manual page for repo rebase
5.SH SYNOPSIS
6.B repo
7\fI\,rebase {\/\fR[\fI\,<project>\/\fR...] \fI\,| -i <project>\/\fR...\fI\,}\/\fR
8.SH DESCRIPTION
9Summary
10.PP
11Rebase local branches on upstream branch
12.SH OPTIONS
13.TP
14\fB\-h\fR, \fB\-\-help\fR
15show this help message and exit
16.TP
17\fB\-\-fail\-fast\fR
18stop rebasing after first error is hit
19.TP
20\fB\-f\fR, \fB\-\-force\-rebase\fR
21pass \fB\-\-force\-rebase\fR to git rebase
22.TP
23\fB\-\-no\-ff\fR
24pass \fB\-\-no\-ff\fR to git rebase
25.TP
26\fB\-\-autosquash\fR
27pass \fB\-\-autosquash\fR to git rebase
28.TP
29\fB\-\-whitespace\fR=\fI\,WS\/\fR
30pass \fB\-\-whitespace\fR to git rebase
31.TP
32\fB\-\-auto\-stash\fR
33stash local modifications before starting
34.TP
35\fB\-m\fR, \fB\-\-onto\-manifest\fR
36rebase onto the manifest version instead of upstream
37HEAD (this helps to make sure the local tree stays
38consistent if you previously synced to a manifest)
39.SS Logging options:
40.TP
41\fB\-v\fR, \fB\-\-verbose\fR
42show all output
43.TP
44\fB\-q\fR, \fB\-\-quiet\fR
45only show errors
46.TP
47\fB\-i\fR, \fB\-\-interactive\fR
48interactive rebase (single project only)
49.PP
50Run `repo help rebase` to view the detailed manual.
51.SH DETAILS
52.PP
53\&'repo rebase' uses git rebase to move local changes in the current topic branch
54to the HEAD of the upstream history, useful when you have made commits in a
55topic branch but need to incorporate new upstream changes "underneath" them.
diff --git a/man/repo-selfupdate.1 b/man/repo-selfupdate.1
new file mode 100644
index 00000000..70c855ab
--- /dev/null
+++ b/man/repo-selfupdate.1
@@ -0,0 +1,35 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo selfupdate" "Repo Manual"
3.SH NAME
4repo \- repo selfupdate - manual page for repo selfupdate
5.SH SYNOPSIS
6.B repo
7\fI\,selfupdate\/\fR
8.SH DESCRIPTION
9Summary
10.PP
11Update repo to the latest version
12.SH OPTIONS
13.TP
14\fB\-h\fR, \fB\-\-help\fR
15show this help message and exit
16.SS Logging options:
17.TP
18\fB\-v\fR, \fB\-\-verbose\fR
19show all output
20.TP
21\fB\-q\fR, \fB\-\-quiet\fR
22only show errors
23.SS repo Version options:
24.TP
25\fB\-\-no\-repo\-verify\fR
26do not verify repo source code
27.PP
28Run `repo help selfupdate` to view the detailed manual.
29.SH DETAILS
30.PP
31The 'repo selfupdate' command upgrades repo to the latest version, if a newer
32version is available.
33.PP
34Normally this is done automatically by 'repo sync' and does not need to be
35performed by an end\-user.
diff --git a/man/repo-smartsync.1 b/man/repo-smartsync.1
new file mode 100644
index 00000000..5d939117
--- /dev/null
+++ b/man/repo-smartsync.1
@@ -0,0 +1,118 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo smartsync" "Repo Manual"
3.SH NAME
4repo \- repo smartsync - manual page for repo smartsync
5.SH SYNOPSIS
6.B repo
7\fI\,smartsync \/\fR[\fI\,<project>\/\fR...]
8.SH DESCRIPTION
9Summary
10.PP
11Update working tree to the latest known good revision
12.SH OPTIONS
13.TP
14\fB\-h\fR, \fB\-\-help\fR
15show this help message and exit
16.TP
17\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
18number of jobs to run in parallel (default: based on
19number of CPU cores)
20.TP
21\fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
22number of network jobs to run in parallel (defaults to
23\fB\-\-jobs\fR)
24.TP
25\fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
26number of local checkout jobs to run in parallel
27(defaults to \fB\-\-jobs\fR)
28.TP
29\fB\-f\fR, \fB\-\-force\-broken\fR
30obsolete option (to be deleted in the future)
31.TP
32\fB\-\-fail\-fast\fR
33stop syncing after first error is hit
34.TP
35\fB\-\-force\-sync\fR
36overwrite an existing git directory if it needs to
37point to a different object directory. WARNING: this
38may cause loss of data
39.TP
40\fB\-\-force\-remove\-dirty\fR
41force remove projects with uncommitted modifications
42if projects no longer exist in the manifest. WARNING:
43this may cause loss of data
44.TP
45\fB\-l\fR, \fB\-\-local\-only\fR
46only update working tree, don't fetch
47.TP
48\fB\-\-no\-manifest\-update\fR, \fB\-\-nmu\fR
49use the existing manifest checkout as\-is. (do not
50update to the latest revision)
51.TP
52\fB\-n\fR, \fB\-\-network\-only\fR
53fetch only, don't update working tree
54.TP
55\fB\-d\fR, \fB\-\-detach\fR
56detach projects back to manifest revision
57.TP
58\fB\-c\fR, \fB\-\-current\-branch\fR
59fetch only current branch from server
60.TP
61\fB\-\-no\-current\-branch\fR
62fetch all branches from server
63.TP
64\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
65temporary manifest to use for this sync
66.TP
67\fB\-\-clone\-bundle\fR
68enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
69.TP
70\fB\-\-no\-clone\-bundle\fR
71disable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
72.TP
73\fB\-u\fR MANIFEST_SERVER_USERNAME, \fB\-\-manifest\-server\-username\fR=\fI\,MANIFEST_SERVER_USERNAME\/\fR
74username to authenticate with the manifest server
75.TP
76\fB\-p\fR MANIFEST_SERVER_PASSWORD, \fB\-\-manifest\-server\-password\fR=\fI\,MANIFEST_SERVER_PASSWORD\/\fR
77password to authenticate with the manifest server
78.TP
79\fB\-\-fetch\-submodules\fR
80fetch submodules from server
81.TP
82\fB\-\-use\-superproject\fR
83use the manifest superproject to sync projects
84.TP
85\fB\-\-no\-use\-superproject\fR
86disable use of manifest superprojects
87.TP
88\fB\-\-tags\fR
89fetch tags
90.TP
91\fB\-\-no\-tags\fR
92don't fetch tags
93.TP
94\fB\-\-optimized\-fetch\fR
95only fetch projects fixed to sha1 if revision does not
96exist locally
97.TP
98\fB\-\-retry\-fetches\fR=\fI\,RETRY_FETCHES\/\fR
99number of times to retry fetches on transient errors
100.TP
101\fB\-\-prune\fR
102delete refs that no longer exist on the remote
103.SS Logging options:
104.TP
105\fB\-v\fR, \fB\-\-verbose\fR
106show all output
107.TP
108\fB\-q\fR, \fB\-\-quiet\fR
109only show errors
110.SS repo Version options:
111.TP
112\fB\-\-no\-repo\-verify\fR
113do not verify repo source code
114.PP
115Run `repo help smartsync` to view the detailed manual.
116.SH DETAILS
117.PP
118The 'repo smartsync' command is a shortcut for sync \fB\-s\fR.
diff --git a/man/repo-stage.1 b/man/repo-stage.1
new file mode 100644
index 00000000..07e1cac6
--- /dev/null
+++ b/man/repo-stage.1
@@ -0,0 +1,30 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo stage" "Repo Manual"
3.SH NAME
4repo \- repo stage - manual page for repo stage
5.SH SYNOPSIS
6.B repo
7\fI\,stage -i \/\fR[\fI\,<project>\/\fR...]
8.SH DESCRIPTION
9Summary
10.PP
11Stage file(s) for commit
12.SH OPTIONS
13.TP
14\fB\-h\fR, \fB\-\-help\fR
15show this help message and exit
16.SS Logging options:
17.TP
18\fB\-v\fR, \fB\-\-verbose\fR
19show all output
20.TP
21\fB\-q\fR, \fB\-\-quiet\fR
22only show errors
23.TP
24\fB\-i\fR, \fB\-\-interactive\fR
25use interactive staging
26.PP
27Run `repo help stage` to view the detailed manual.
28.SH DETAILS
29.PP
30The 'repo stage' command stages files to prepare the next commit.
diff --git a/man/repo-start.1 b/man/repo-start.1
new file mode 100644
index 00000000..b00a31f4
--- /dev/null
+++ b/man/repo-start.1
@@ -0,0 +1,41 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo start" "Repo Manual"
3.SH NAME
4repo \- repo start - manual page for repo start
5.SH SYNOPSIS
6.B repo
7\fI\,start <newbranchname> \/\fR[\fI\,--all | <project>\/\fR...]
8.SH DESCRIPTION
9Summary
10.PP
11Start a new branch for development
12.SH OPTIONS
13.TP
14\fB\-h\fR, \fB\-\-help\fR
15show this help message and exit
16.TP
17\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
18number of jobs to run in parallel (default: based on
19number of CPU cores)
20.TP
21\fB\-\-all\fR
22begin branch in all projects
23.TP
24\fB\-r\fR REVISION, \fB\-\-rev\fR=\fI\,REVISION\/\fR, \fB\-\-revision\fR=\fI\,REVISION\/\fR
25point branch at this revision instead of upstream
26.TP
27\fB\-\-head\fR, \fB\-\-HEAD\fR
28abbreviation for \fB\-\-rev\fR HEAD
29.SS Logging options:
30.TP
31\fB\-v\fR, \fB\-\-verbose\fR
32show all output
33.TP
34\fB\-q\fR, \fB\-\-quiet\fR
35only show errors
36.PP
37Run `repo help start` to view the detailed manual.
38.SH DETAILS
39.PP
40\&'repo start' begins a new branch of development, starting from the revision
41specified in the manifest.
diff --git a/man/repo-status.1 b/man/repo-status.1
new file mode 100644
index 00000000..fbae2c5d
--- /dev/null
+++ b/man/repo-status.1
@@ -0,0 +1,98 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo status" "Repo Manual"
3.SH NAME
4repo \- repo status - manual page for repo status
5.SH SYNOPSIS
6.B repo
7\fI\,status \/\fR[\fI\,<project>\/\fR...]
8.SH DESCRIPTION
9Summary
10.PP
11Show the working tree status
12.SH OPTIONS
13.TP
14\fB\-h\fR, \fB\-\-help\fR
15show this help message and exit
16.TP
17\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
18number of jobs to run in parallel (default: based on
19number of CPU cores)
20.TP
21\fB\-o\fR, \fB\-\-orphans\fR
22include objects in working directory outside of repo
23projects
24.SS Logging options:
25.TP
26\fB\-v\fR, \fB\-\-verbose\fR
27show all output
28.TP
29\fB\-q\fR, \fB\-\-quiet\fR
30only show errors
31.PP
32Run `repo help status` to view the detailed manual.
33.SH DETAILS
34.PP
35\&'repo status' compares the working tree to the staging area (aka index), and the
36most recent commit on this branch (HEAD), in each project specified. A summary
37is displayed, one line per file where there is a difference between these three
38states.
39.PP
40The \fB\-j\fR/\-\-jobs option can be used to run multiple status queries in parallel.
41.PP
42The \fB\-o\fR/\-\-orphans option can be used to show objects that are in the working
43directory, but not associated with a repo project. This includes unmanaged
44top\-level files and directories, but also includes deeper items. For example, if
45dir/subdir/proj1 and dir/subdir/proj2 are repo projects, dir/subdir/proj3 will
46be shown if it is not known to repo.
47.PP
48Status Display
49.PP
50The status display is organized into three columns of information, for example
51if the file 'subcmds/status.py' is modified in the project 'repo' on branch
52\&'devwork':
53.TP
54project repo/
55branch devwork
56.TP
57\fB\-m\fR
58subcmds/status.py
59.PP
60The first column explains how the staging area (index) differs from the last
61commit (HEAD). Its values are always displayed in upper case and have the
62following meanings:
63.TP
64\-:
65no difference
66.TP
67A:
68added (not in HEAD, in index )
69.TP
70M:
71modified ( in HEAD, in index, different content )
72.TP
73D:
74deleted ( in HEAD, not in index )
75.TP
76R:
77renamed (not in HEAD, in index, path changed )
78.TP
79C:
80copied (not in HEAD, in index, copied from another)
81.TP
82T:
83mode changed ( in HEAD, in index, same content )
84.TP
85U:
86unmerged; conflict resolution required
87.PP
88The second column explains how the working directory differs from the index. Its
89values are always displayed in lower case and have the following meanings:
90.TP
91\-:
92new / unknown (not in index, in work tree )
93.TP
94m:
95modified ( in index, in work tree, modified )
96.TP
97d:
98deleted ( in index, not in work tree )
diff --git a/man/repo-sync.1 b/man/repo-sync.1
new file mode 100644
index 00000000..c87c9701
--- /dev/null
+++ b/man/repo-sync.1
@@ -0,0 +1,209 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo sync" "Repo Manual"
3.SH NAME
4repo \- repo sync - manual page for repo sync
5.SH SYNOPSIS
6.B repo
7\fI\,sync \/\fR[\fI\,<project>\/\fR...]
8.SH DESCRIPTION
9Summary
10.PP
11Update working tree to the latest revision
12.SH OPTIONS
13.TP
14\fB\-h\fR, \fB\-\-help\fR
15show this help message and exit
16.TP
17\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
18number of jobs to run in parallel (default: based on
19number of CPU cores)
20.TP
21\fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
22number of network jobs to run in parallel (defaults to
23\fB\-\-jobs\fR)
24.TP
25\fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
26number of local checkout jobs to run in parallel
27(defaults to \fB\-\-jobs\fR)
28.TP
29\fB\-f\fR, \fB\-\-force\-broken\fR
30obsolete option (to be deleted in the future)
31.TP
32\fB\-\-fail\-fast\fR
33stop syncing after first error is hit
34.TP
35\fB\-\-force\-sync\fR
36overwrite an existing git directory if it needs to
37point to a different object directory. WARNING: this
38may cause loss of data
39.TP
40\fB\-\-force\-remove\-dirty\fR
41force remove projects with uncommitted modifications
42if projects no longer exist in the manifest. WARNING:
43this may cause loss of data
44.TP
45\fB\-l\fR, \fB\-\-local\-only\fR
46only update working tree, don't fetch
47.TP
48\fB\-\-no\-manifest\-update\fR, \fB\-\-nmu\fR
49use the existing manifest checkout as\-is. (do not
50update to the latest revision)
51.TP
52\fB\-n\fR, \fB\-\-network\-only\fR
53fetch only, don't update working tree
54.TP
55\fB\-d\fR, \fB\-\-detach\fR
56detach projects back to manifest revision
57.TP
58\fB\-c\fR, \fB\-\-current\-branch\fR
59fetch only current branch from server
60.TP
61\fB\-\-no\-current\-branch\fR
62fetch all branches from server
63.TP
64\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
65temporary manifest to use for this sync
66.TP
67\fB\-\-clone\-bundle\fR
68enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
69.TP
70\fB\-\-no\-clone\-bundle\fR
71disable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
72.TP
73\fB\-u\fR MANIFEST_SERVER_USERNAME, \fB\-\-manifest\-server\-username\fR=\fI\,MANIFEST_SERVER_USERNAME\/\fR
74username to authenticate with the manifest server
75.TP
76\fB\-p\fR MANIFEST_SERVER_PASSWORD, \fB\-\-manifest\-server\-password\fR=\fI\,MANIFEST_SERVER_PASSWORD\/\fR
77password to authenticate with the manifest server
78.TP
79\fB\-\-fetch\-submodules\fR
80fetch submodules from server
81.TP
82\fB\-\-use\-superproject\fR
83use the manifest superproject to sync projects
84.TP
85\fB\-\-no\-use\-superproject\fR
86disable use of manifest superprojects
87.TP
88\fB\-\-tags\fR
89fetch tags
90.TP
91\fB\-\-no\-tags\fR
92don't fetch tags
93.TP
94\fB\-\-optimized\-fetch\fR
95only fetch projects fixed to sha1 if revision does not
96exist locally
97.TP
98\fB\-\-retry\-fetches\fR=\fI\,RETRY_FETCHES\/\fR
99number of times to retry fetches on transient errors
100.TP
101\fB\-\-prune\fR
102delete refs that no longer exist on the remote
103.TP
104\fB\-s\fR, \fB\-\-smart\-sync\fR
105smart sync using manifest from the latest known good
106build
107.TP
108\fB\-t\fR SMART_TAG, \fB\-\-smart\-tag\fR=\fI\,SMART_TAG\/\fR
109smart sync using manifest from a known tag
110.SS Logging options:
111.TP
112\fB\-v\fR, \fB\-\-verbose\fR
113show all output
114.TP
115\fB\-q\fR, \fB\-\-quiet\fR
116only show errors
117.SS repo Version options:
118.TP
119\fB\-\-no\-repo\-verify\fR
120do not verify repo source code
121.PP
122Run `repo help sync` to view the detailed manual.
123.SH DETAILS
124.PP
125The 'repo sync' command synchronizes local project directories with the remote
126repositories specified in the manifest. If a local project does not yet exist,
127it will clone a new local directory from the remote repository and set up
128tracking branches as specified in the manifest. If the local project already
129exists, 'repo sync' will update the remote branches and rebase any new local
130changes on top of the new remote changes.
131.PP
132\&'repo sync' will synchronize all projects listed at the command line. Projects
133can be specified either by name, or by a relative or absolute path to the
134project's local directory. If no projects are specified, 'repo sync' will
135synchronize all projects listed in the manifest.
136.PP
137The \fB\-d\fR/\-\-detach option can be used to switch specified projects back to the
138manifest revision. This option is especially helpful if the project is currently
139on a topic branch, but the manifest revision is temporarily needed.
140.PP
141The \fB\-s\fR/\-\-smart\-sync option can be used to sync to a known good build as
142specified by the manifest\-server element in the current manifest. The
143\fB\-t\fR/\-\-smart\-tag option is similar and allows you to specify a custom tag/label.
144.PP
145The \fB\-u\fR/\-\-manifest\-server\-username and \fB\-p\fR/\-\-manifest\-server\-password options can
146be used to specify a username and password to authenticate with the manifest
147server when using the \fB\-s\fR or \fB\-t\fR option.
148.PP
149If \fB\-u\fR and \fB\-p\fR are not specified when using the \fB\-s\fR or \fB\-t\fR option, 'repo sync' will
150attempt to read authentication credentials for the manifest server from the
151user's .netrc file.
152.PP
153\&'repo sync' will not use authentication credentials from \fB\-u\fR/\-p or .netrc if the
154manifest server specified in the manifest file already includes credentials.
155.PP
156By default, all projects will be synced. The \fB\-\-fail\-fast\fR option can be used to
157halt syncing as soon as possible when the first project fails to sync.
158.PP
159The \fB\-\-force\-sync\fR option can be used to overwrite existing git directories if
160they have previously been linked to a different object directory. WARNING: This
161may cause data to be lost since refs may be removed when overwriting.
162.PP
163The \fB\-\-force\-remove\-dirty\fR option can be used to remove previously used projects
164with uncommitted changes. WARNING: This may cause data to be lost since
165uncommitted changes may be removed with projects that no longer exist in the
166manifest.
167.PP
168The \fB\-\-no\-clone\-bundle\fR option disables any attempt to use \fI\,$URL/clone.bundle\/\fP to
169bootstrap a new Git repository from a resumeable bundle file on a content
170delivery network. This may be necessary if there are problems with the local
171Python HTTP client or proxy configuration, but the Git binary works.
172.PP
173The \fB\-\-fetch\-submodules\fR option enables fetching Git submodules of a project from
174server.
175.PP
176The \fB\-c\fR/\-\-current\-branch option can be used to only fetch objects that are on the
177branch specified by a project's revision.
178.PP
179The \fB\-\-optimized\-fetch\fR option can be used to only fetch projects that are fixed
180to a sha1 revision if the sha1 revision does not already exist locally.
181.PP
182The \fB\-\-prune\fR option can be used to remove any refs that no longer exist on the
183remote.
184.PP
185SSH Connections
186.PP
187If at least one project remote URL uses an SSH connection (ssh://, git+ssh://,
188or user@host:path syntax) repo will automatically enable the SSH ControlMaster
189option when connecting to that host. This feature permits other projects in the
190same 'repo sync' session to reuse the same SSH tunnel, saving connection setup
191overheads.
192.PP
193To disable this behavior on UNIX platforms, set the GIT_SSH environment variable
194to 'ssh'. For example:
195.IP
196export GIT_SSH=ssh
197repo sync
198.PP
199Compatibility
200.PP
201This feature is automatically disabled on Windows, due to the lack of UNIX
202domain socket support.
203.PP
204This feature is not compatible with url.insteadof rewrites in the user's
205~/.gitconfig. 'repo sync' is currently not able to perform the rewrite early
206enough to establish the ControlMaster tunnel.
207.PP
208If the remote SSH daemon is Gerrit Code Review, version 2.0.10 or later is
209required to fix a server side protocol bug.
diff --git a/man/repo-upload.1 b/man/repo-upload.1
new file mode 100644
index 00000000..36a0daca
--- /dev/null
+++ b/man/repo-upload.1
@@ -0,0 +1,175 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo upload" "Repo Manual"
3.SH NAME
4repo \- repo upload - manual page for repo upload
5.SH SYNOPSIS
6.B repo
7\fI\,upload \/\fR[\fI\,--re --cc\/\fR] [\fI\,<project>\/\fR]...
8.SH DESCRIPTION
9Summary
10.PP
11Upload changes for code review
12.SH OPTIONS
13.TP
14\fB\-h\fR, \fB\-\-help\fR
15show this help message and exit
16.TP
17\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
18number of jobs to run in parallel (default: based on
19number of CPU cores)
20.TP
21\fB\-t\fR
22send local branch name to Gerrit Code Review
23.TP
24\fB\-\-hashtag\fR=\fI\,HASHTAGS\/\fR, \fB\-\-ht\fR=\fI\,HASHTAGS\/\fR
25add hashtags (comma delimited) to the review
26.TP
27\fB\-\-hashtag\-branch\fR, \fB\-\-htb\fR
28add local branch name as a hashtag
29.TP
30\fB\-l\fR LABELS, \fB\-\-label\fR=\fI\,LABELS\/\fR
31add a label when uploading
32.TP
33\fB\-\-re\fR=\fI\,REVIEWERS\/\fR, \fB\-\-reviewers\fR=\fI\,REVIEWERS\/\fR
34request reviews from these people
35.TP
36\fB\-\-cc\fR=\fI\,CC\/\fR
37also send email to these email addresses
38.TP
39\fB\-\-br\fR=\fI\,BRANCH\/\fR, \fB\-\-branch\fR=\fI\,BRANCH\/\fR
40(local) branch to upload
41.TP
42\fB\-c\fR, \fB\-\-current\-branch\fR
43upload current git branch
44.TP
45\fB\-\-no\-current\-branch\fR
46upload all git branches
47.TP
48\fB\-\-ne\fR, \fB\-\-no\-emails\fR
49do not send e\-mails on upload
50.TP
51\fB\-p\fR, \fB\-\-private\fR
52upload as a private change (deprecated; use \fB\-\-wip\fR)
53.TP
54\fB\-w\fR, \fB\-\-wip\fR
55upload as a work\-in\-progress change
56.TP
57\fB\-o\fR PUSH_OPTIONS, \fB\-\-push\-option\fR=\fI\,PUSH_OPTIONS\/\fR
58additional push options to transmit
59.TP
60\fB\-D\fR BRANCH, \fB\-\-destination\fR=\fI\,BRANCH\/\fR, \fB\-\-dest\fR=\fI\,BRANCH\/\fR
61submit for review on this target branch
62.TP
63\fB\-n\fR, \fB\-\-dry\-run\fR
64do everything except actually upload the CL
65.TP
66\fB\-y\fR, \fB\-\-yes\fR
67answer yes to all safe prompts
68.TP
69\fB\-\-no\-cert\-checks\fR
70disable verifying ssl certs (unsafe)
71.SS Logging options:
72.TP
73\fB\-v\fR, \fB\-\-verbose\fR
74show all output
75.TP
76\fB\-q\fR, \fB\-\-quiet\fR
77only show errors
78.SS pre\-upload hooks:
79.TP
80\fB\-\-no\-verify\fR
81Do not run the pre\-upload hook.
82.TP
83\fB\-\-verify\fR
84Run the pre\-upload hook without prompting.
85.TP
86\fB\-\-ignore\-hooks\fR
87Do not abort if pre\-upload hooks fail.
88.PP
89Run `repo help upload` to view the detailed manual.
90.SH DETAILS
91.PP
92The 'repo upload' command is used to send changes to the Gerrit Code Review
93system. It searches for topic branches in local projects that have not yet been
94published for review. If multiple topic branches are found, 'repo upload' opens
95an editor to allow the user to select which branches to upload.
96.PP
97\&'repo upload' searches for uploadable changes in all projects listed at the
98command line. Projects can be specified either by name, or by a relative or
99absolute path to the project's local directory. If no projects are specified,
100\&'repo upload' will search for uploadable changes in all projects listed in the
101manifest.
102.PP
103If the \fB\-\-reviewers\fR or \fB\-\-cc\fR options are passed, those emails are added to the
104respective list of users, and emails are sent to any new users. Users passed as
105\fB\-\-reviewers\fR must already be registered with the code review system, or the
106upload will fail.
107.PP
108Configuration
109.PP
110review.URL.autoupload:
111.PP
112To disable the "Upload ... (y/N)?" prompt, you can set a per\-project or global
113Git configuration option. If review.URL.autoupload is set to "true" then repo
114will assume you always answer "y" at the prompt, and will not prompt you
115further. If it is set to "false" then repo will assume you always answer "n",
116and will abort.
117.PP
118review.URL.autoreviewer:
119.PP
120To automatically append a user or mailing list to reviews, you can set a
121per\-project or global Git option to do so.
122.PP
123review.URL.autocopy:
124.PP
125To automatically copy a user or mailing list to all uploaded reviews, you can
126set a per\-project or global Git option to do so. Specifically,
127review.URL.autocopy can be set to a comma separated list of reviewers who you
128always want copied on all uploads with a non\-empty \fB\-\-re\fR argument.
129.PP
130review.URL.username:
131.PP
132Override the username used to connect to Gerrit Code Review. By default the
133local part of the email address is used.
134.PP
135The URL must match the review URL listed in the manifest XML file, or in the
136\&.git/config within the project. For example:
137.IP
138[remote "origin"]
139.IP
140url = git://git.example.com/project.git
141review = http://review.example.com/
142.IP
143[review "http://review.example.com/"]
144.IP
145autoupload = true
146autocopy = johndoe@company.com,my\-team\-alias@company.com
147.PP
148review.URL.uploadtopic:
149.PP
150To add a topic branch whenever uploading a commit, you can set a per\-project or
151global Git option to do so. If review.URL.uploadtopic is set to "true" then repo
152will assume you always want the equivalent of the \fB\-t\fR option to the repo command.
153If unset or set to "false" then repo will make use of only the command line
154option.
155.PP
156review.URL.uploadhashtags:
157.PP
158To add hashtags whenever uploading a commit, you can set a per\-project or global
159Git option to do so. The value of review.URL.uploadhashtags will be used as
160comma delimited hashtags like the \fB\-\-hashtag\fR option.
161.PP
162review.URL.uploadlabels:
163.PP
164To add labels whenever uploading a commit, you can set a per\-project or global
165Git option to do so. The value of review.URL.uploadlabels will be used as comma
166delimited labels like the \fB\-\-label\fR option.
167.PP
168review.URL.uploadnotify:
169.PP
170Control e\-mail notifications when uploading.
171https://gerrit\-review.googlesource.com/Documentation/user\-upload.html#notify
172.PP
173References
174.PP
175Gerrit Code Review: https://www.gerritcodereview.com/
diff --git a/man/repo-version.1 b/man/repo-version.1
new file mode 100644
index 00000000..cc703f61
--- /dev/null
+++ b/man/repo-version.1
@@ -0,0 +1,24 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo version" "Repo Manual"
3.SH NAME
4repo \- repo version - manual page for repo version
5.SH SYNOPSIS
6.B repo
7\fI\,version\/\fR
8.SH DESCRIPTION
9Summary
10.PP
11Display the version of repo
12.SH OPTIONS
13.TP
14\fB\-h\fR, \fB\-\-help\fR
15show this help message and exit
16.SS Logging options:
17.TP
18\fB\-v\fR, \fB\-\-verbose\fR
19show all output
20.TP
21\fB\-q\fR, \fB\-\-quiet\fR
22only show errors
23.PP
24Run `repo help version` to view the detailed manual.
diff --git a/man/repo.1 b/man/repo.1
new file mode 100644
index 00000000..4aa76380
--- /dev/null
+++ b/man/repo.1
@@ -0,0 +1,133 @@
1.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2.TH REPO "1" "July 2021" "repo" "Repo Manual"
3.SH NAME
4repo \- 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
11show this help message and exit
12.TP
13\fB\-\-help\-all\fR
14show this help message with all subcommands and exit
15.TP
16\fB\-p\fR, \fB\-\-paginate\fR
17display command output in the pager
18.TP
19\fB\-\-no\-pager\fR
20disable the pager
21.TP
22\fB\-\-color\fR=\fI\,COLOR\/\fR
23control color usage: auto, always, never
24.TP
25\fB\-\-trace\fR
26trace git command execution (REPO_TRACE=1)
27.TP
28\fB\-\-trace\-python\fR
29trace python command execution
30.TP
31\fB\-\-time\fR
32time repo command execution
33.TP
34\fB\-\-version\fR
35display this version of repo
36.TP
37\fB\-\-show\-toplevel\fR
38display the path of the top\-level directory of the
39repo client checkout
40.TP
41\fB\-\-event\-log\fR=\fI\,EVENT_LOG\/\fR
42filename of event log to append timeline to
43.TP
44\fB\-\-git\-trace2\-event\-log\fR=\fI\,GIT_TRACE2_EVENT_LOG\/\fR
45directory to write git trace2 event log to
46.SS "The complete list of recognized repo commands are:"
47.TP
48abandon
49Permanently abandon a development branch
50.TP
51branch
52View current topic branches
53.TP
54branches
55View current topic branches
56.TP
57checkout
58Checkout a branch for development
59.TP
60cherry\-pick
61Cherry\-pick a change.
62.TP
63diff
64Show changes between commit and working tree
65.TP
66diffmanifests
67Manifest diff utility
68.TP
69download
70Download and checkout a change
71.TP
72forall
73Run a shell command in each project
74.TP
75gitc\-delete
76Delete a GITC Client.
77.TP
78gitc\-init
79Initialize a GITC Client.
80.TP
81grep
82Print lines matching a pattern
83.TP
84help
85Display detailed help on a command
86.TP
87info
88Get info on the manifest branch, current branch or unmerged branches
89.TP
90init
91Initialize a repo client checkout in the current directory
92.TP
93list
94List projects and their associated directories
95.TP
96manifest
97Manifest inspection utility
98.TP
99overview
100Display overview of unmerged project branches
101.TP
102prune
103Prune (delete) already merged topics
104.TP
105rebase
106Rebase local branches on upstream branch
107.TP
108selfupdate
109Update repo to the latest version
110.TP
111smartsync
112Update working tree to the latest known good revision
113.TP
114stage
115Stage file(s) for commit
116.TP
117start
118Start a new branch for development
119.TP
120status
121Show the working tree status
122.TP
123sync
124Update working tree to the latest revision
125.TP
126upload
127Upload changes for code review
128.TP
129version
130Display the version of repo
131.PP
132See 'repo help <command>' for more information on a specific command.
133Bug reports: https://bugs.chromium.org/p/gerrit/issues/entry?template=Repo+tool+issue
diff --git a/manifest_xml.py b/manifest_xml.py
index 3814a25a..68ead53c 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,33 +12,34 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function 15import collections
18import itertools 16import itertools
19import os 17import os
18import platform
20import re 19import re
21import sys 20import sys
22import xml.dom.minidom 21import xml.dom.minidom
23 22import urllib.parse
24from pyversion import is_python3
25if is_python3():
26 import urllib.parse
27else:
28 import imp
29 import urlparse
30 urllib = imp.new_module('urllib')
31 urllib.parse = urlparse
32 23
33import gitc_utils 24import gitc_utils
34from git_config import GitConfig 25from git_config import GitConfig, IsId
35from git_refs import R_HEADS, HEAD 26from git_refs import R_HEADS, HEAD
36import platform_utils 27import platform_utils
37from project import RemoteSpec, Project, MetaProject 28from project import Annotation, RemoteSpec, Project, MetaProject
38from error import ManifestParseError, ManifestInvalidRevisionError 29from error import (ManifestParseError, ManifestInvalidPathError,
30 ManifestInvalidRevisionError)
31from wrapper import Wrapper
39 32
40MANIFEST_FILE_NAME = 'manifest.xml' 33MANIFEST_FILE_NAME = 'manifest.xml'
41LOCAL_MANIFEST_NAME = 'local_manifest.xml' 34LOCAL_MANIFEST_NAME = 'local_manifest.xml'
42LOCAL_MANIFESTS_DIR_NAME = 'local_manifests' 35LOCAL_MANIFESTS_DIR_NAME = 'local_manifests'
43 36
37# Add all projects from local manifest into a group.
38LOCAL_MANIFEST_GROUP_PREFIX = 'local:'
39
40# ContactInfo has the self-registered bug url, supplied by the manifest authors.
41ContactInfo = collections.namedtuple('ContactInfo', 'bugurl')
42
44# urljoin gets confused if the scheme is not known. 43# urljoin gets confused if the scheme is not known.
45urllib.parse.uses_relative.extend([ 44urllib.parse.uses_relative.extend([
46 'ssh', 45 'ssh',
@@ -55,6 +54,61 @@ urllib.parse.uses_netloc.extend([
55 'sso', 54 'sso',
56 'rpc']) 55 'rpc'])
57 56
57
58def XmlBool(node, attr, default=None):
59 """Determine boolean value of |node|'s |attr|.
60
61 Invalid values will issue a non-fatal warning.
62
63 Args:
64 node: XML node whose attributes we access.
65 attr: The attribute to access.
66 default: If the attribute is not set (value is empty), then use this.
67
68 Returns:
69 True if the attribute is a valid string representing true.
70 False if the attribute is a valid string representing false.
71 |default| otherwise.
72 """
73 value = node.getAttribute(attr)
74 s = value.lower()
75 if s == '':
76 return default
77 elif s in {'yes', 'true', '1'}:
78 return True
79 elif s in {'no', 'false', '0'}:
80 return False
81 else:
82 print('warning: manifest: %s="%s": ignoring invalid XML boolean' %
83 (attr, value), file=sys.stderr)
84 return default
85
86
87def XmlInt(node, attr, default=None):
88 """Determine integer value of |node|'s |attr|.
89
90 Args:
91 node: XML node whose attributes we access.
92 attr: The attribute to access.
93 default: If the attribute is not set (value is empty), then use this.
94
95 Returns:
96 The number if the attribute is a valid number.
97
98 Raises:
99 ManifestParseError: The number is invalid.
100 """
101 value = node.getAttribute(attr)
102 if not value:
103 return default
104
105 try:
106 return int(value)
107 except ValueError:
108 raise ManifestParseError('manifest: invalid %s="%s" integer' %
109 (attr, value))
110
111
58class _Default(object): 112class _Default(object):
59 """Project defaults within the manifest.""" 113 """Project defaults within the manifest."""
60 114
@@ -68,11 +122,16 @@ class _Default(object):
68 sync_tags = True 122 sync_tags = True
69 123
70 def __eq__(self, other): 124 def __eq__(self, other):
125 if not isinstance(other, _Default):
126 return False
71 return self.__dict__ == other.__dict__ 127 return self.__dict__ == other.__dict__
72 128
73 def __ne__(self, other): 129 def __ne__(self, other):
130 if not isinstance(other, _Default):
131 return True
74 return self.__dict__ != other.__dict__ 132 return self.__dict__ != other.__dict__
75 133
134
76class _XmlRemote(object): 135class _XmlRemote(object):
77 def __init__(self, 136 def __init__(self,
78 name, 137 name,
@@ -90,14 +149,22 @@ class _XmlRemote(object):
90 self.reviewUrl = review 149 self.reviewUrl = review
91 self.revision = revision 150 self.revision = revision
92 self.resolvedFetchUrl = self._resolveFetchUrl() 151 self.resolvedFetchUrl = self._resolveFetchUrl()
152 self.annotations = []
93 153
94 def __eq__(self, other): 154 def __eq__(self, other):
95 return self.__dict__ == other.__dict__ 155 if not isinstance(other, _XmlRemote):
156 return False
157 return (sorted(self.annotations) == sorted(other.annotations) and
158 self.name == other.name and self.fetchUrl == other.fetchUrl and
159 self.pushUrl == other.pushUrl and self.remoteAlias == other.remoteAlias
160 and self.reviewUrl == other.reviewUrl and self.revision == other.revision)
96 161
97 def __ne__(self, other): 162 def __ne__(self, other):
98 return self.__dict__ != other.__dict__ 163 return not self.__eq__(other)
99 164
100 def _resolveFetchUrl(self): 165 def _resolveFetchUrl(self):
166 if self.fetchUrl is None:
167 return ''
101 url = self.fetchUrl.rstrip('/') 168 url = self.fetchUrl.rstrip('/')
102 manifestUrl = self.manifestUrl.rstrip('/') 169 manifestUrl = self.manifestUrl.rstrip('/')
103 # urljoin will gets confused over quite a few things. The ones we care 170 # urljoin will gets confused over quite a few things. The ones we care
@@ -126,25 +193,48 @@ class _XmlRemote(object):
126 orig_name=self.name, 193 orig_name=self.name,
127 fetchUrl=self.fetchUrl) 194 fetchUrl=self.fetchUrl)
128 195
196 def AddAnnotation(self, name, value, keep):
197 self.annotations.append(Annotation(name, value, keep))
198
199
129class XmlManifest(object): 200class XmlManifest(object):
130 """manages the repo configuration file""" 201 """manages the repo configuration file"""
131 202
132 def __init__(self, repodir): 203 def __init__(self, repodir, manifest_file, local_manifests=None):
204 """Initialize.
205
206 Args:
207 repodir: Path to the .repo/ dir for holding all internal checkout state.
208 It must be in the top directory of the repo client checkout.
209 manifest_file: Full path to the manifest file to parse. This will usually
210 be |repodir|/|MANIFEST_FILE_NAME|.
211 local_manifests: Full path to the directory of local override manifests.
212 This will usually be |repodir|/|LOCAL_MANIFESTS_DIR_NAME|.
213 """
214 # TODO(vapier): Move this out of this class.
215 self.globalConfig = GitConfig.ForUser()
216
133 self.repodir = os.path.abspath(repodir) 217 self.repodir = os.path.abspath(repodir)
134 self.topdir = os.path.dirname(self.repodir) 218 self.topdir = os.path.dirname(self.repodir)
135 self.manifestFile = os.path.join(self.repodir, MANIFEST_FILE_NAME) 219 self.manifestFile = manifest_file
136 self.globalConfig = GitConfig.ForUser() 220 self.local_manifests = local_manifests
137 self.localManifestWarning = False
138 self.isGitcClient = False
139 self._load_local_manifests = True 221 self._load_local_manifests = True
140 222
141 self.repoProject = MetaProject(self, 'repo', 223 self.repoProject = MetaProject(self, 'repo',
142 gitdir = os.path.join(repodir, 'repo/.git'), 224 gitdir=os.path.join(repodir, 'repo/.git'),
143 worktree = os.path.join(repodir, 'repo')) 225 worktree=os.path.join(repodir, 'repo'))
144 226
145 self.manifestProject = MetaProject(self, 'manifests', 227 mp = MetaProject(self, 'manifests',
146 gitdir = os.path.join(repodir, 'manifests.git'), 228 gitdir=os.path.join(repodir, 'manifests.git'),
147 worktree = os.path.join(repodir, 'manifests')) 229 worktree=os.path.join(repodir, 'manifests'))
230 self.manifestProject = mp
231
232 # This is a bit hacky, but we're in a chicken & egg situation: all the
233 # normal repo settings live in the manifestProject which we just setup
234 # above, so we couldn't easily query before that. We assume Project()
235 # init doesn't care if this changes afterwards.
236 if os.path.exists(mp.gitdir) and mp.config.GetBoolean('repo.worktree'):
237 mp.use_git_worktrees = True
148 238
149 self._Unload() 239 self._Unload()
150 240
@@ -179,12 +269,26 @@ class XmlManifest(object):
179 """ 269 """
180 self.Override(name) 270 self.Override(name)
181 271
182 try: 272 # Old versions of repo would generate symlinks we need to clean up.
183 if os.path.lexists(self.manifestFile): 273 platform_utils.remove(self.manifestFile, missing_ok=True)
184 platform_utils.remove(self.manifestFile) 274 # This file is interpreted as if it existed inside the manifest repo.
185 platform_utils.symlink(os.path.join('manifests', name), self.manifestFile) 275 # That allows us to use <include> with the relative file name.
186 except OSError as e: 276 with open(self.manifestFile, 'w') as fp:
187 raise ManifestParseError('cannot link manifest %s: %s' % (name, str(e))) 277 fp.write("""<?xml version="1.0" encoding="UTF-8"?>
278<!--
279DO NOT EDIT THIS FILE! It is generated by repo and changes will be discarded.
280If you want to use a different manifest, use `repo init -m <file>` instead.
281
282If you want to customize your checkout by overriding manifest settings, use
283the local_manifests/ directory instead.
284
285For more information on repo manifests, check out:
286https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
287-->
288<manifest>
289 <include name="%s" />
290</manifest>
291""" % (name,))
188 292
189 def _RemoteToXml(self, r, doc, root): 293 def _RemoteToXml(self, r, doc, root):
190 e = doc.createElement('remote') 294 e = doc.createElement('remote')
@@ -200,18 +304,28 @@ class XmlManifest(object):
200 if r.revision is not None: 304 if r.revision is not None:
201 e.setAttribute('revision', r.revision) 305 e.setAttribute('revision', r.revision)
202 306
203 def _ParseGroups(self, groups): 307 for a in r.annotations:
204 return [x for x in re.split(r'[,\s]+', groups) if x] 308 if a.keep == 'true':
309 ae = doc.createElement('annotation')
310 ae.setAttribute('name', a.name)
311 ae.setAttribute('value', a.value)
312 e.appendChild(ae)
313
314 def _ParseList(self, field):
315 """Parse fields that contain flattened lists.
205 316
206 def Save(self, fd, peg_rev=False, peg_rev_upstream=True, groups=None): 317 These are whitespace & comma separated. Empty elements will be discarded.
207 """Write the current manifest out to the given file descriptor.
208 """ 318 """
319 return [x for x in re.split(r'[,\s]+', field) if x]
320
321 def ToXml(self, peg_rev=False, peg_rev_upstream=True, peg_rev_dest_branch=True, groups=None):
322 """Return the current manifest XML."""
209 mp = self.manifestProject 323 mp = self.manifestProject
210 324
211 if groups is None: 325 if groups is None:
212 groups = mp.config.GetString('manifest.groups') 326 groups = mp.config.GetString('manifest.groups')
213 if groups: 327 if groups:
214 groups = self._ParseGroups(groups) 328 groups = self._ParseList(groups)
215 329
216 doc = xml.dom.minidom.Document() 330 doc = xml.dom.minidom.Document()
217 root = doc.createElement('manifest') 331 root = doc.createElement('manifest')
@@ -223,7 +337,7 @@ class XmlManifest(object):
223 if self.notice: 337 if self.notice:
224 notice_element = root.appendChild(doc.createElement('notice')) 338 notice_element = root.appendChild(doc.createElement('notice'))
225 notice_lines = self.notice.splitlines() 339 notice_lines = self.notice.splitlines()
226 indented_notice = ('\n'.join(" "*4 + line for line in notice_lines))[4:] 340 indented_notice = ('\n'.join(" " * 4 + line for line in notice_lines))[4:]
227 notice_element.appendChild(doc.createTextNode(indented_notice)) 341 notice_element.appendChild(doc.createTextNode(indented_notice))
228 342
229 d = self.default 343 d = self.default
@@ -308,10 +422,19 @@ class XmlManifest(object):
308 # Only save the origin if the origin is not a sha1, and the default 422 # Only save the origin if the origin is not a sha1, and the default
309 # isn't our value 423 # isn't our value
310 e.setAttribute('upstream', p.revisionExpr) 424 e.setAttribute('upstream', p.revisionExpr)
425
426 if peg_rev_dest_branch:
427 if p.dest_branch:
428 e.setAttribute('dest-branch', p.dest_branch)
429 elif value != p.revisionExpr:
430 e.setAttribute('dest-branch', p.revisionExpr)
431
311 else: 432 else:
312 revision = self.remotes[p.remote.orig_name].revision or d.revisionExpr 433 revision = self.remotes[p.remote.orig_name].revision or d.revisionExpr
313 if not revision or revision != p.revisionExpr: 434 if not revision or revision != p.revisionExpr:
314 e.setAttribute('revision', p.revisionExpr) 435 e.setAttribute('revision', p.revisionExpr)
436 elif p.revisionId:
437 e.setAttribute('revision', p.revisionId)
315 if (p.upstream and (p.upstream != p.revisionExpr or 438 if (p.upstream and (p.upstream != p.revisionExpr or
316 p.upstream != d.upstreamExpr)): 439 p.upstream != d.upstreamExpr)):
317 e.setAttribute('upstream', p.upstream) 440 e.setAttribute('upstream', p.upstream)
@@ -372,11 +495,84 @@ class XmlManifest(object):
372 ' '.join(self._repo_hooks_project.enabled_repo_hooks)) 495 ' '.join(self._repo_hooks_project.enabled_repo_hooks))
373 root.appendChild(e) 496 root.appendChild(e)
374 497
498 if self._superproject:
499 root.appendChild(doc.createTextNode(''))
500 e = doc.createElement('superproject')
501 e.setAttribute('name', self._superproject['name'])
502 remoteName = None
503 if d.remote:
504 remoteName = d.remote.name
505 remote = self._superproject.get('remote')
506 if not d.remote or remote.orig_name != remoteName:
507 remoteName = remote.orig_name
508 e.setAttribute('remote', remoteName)
509 revision = remote.revision or d.revisionExpr
510 if not revision or revision != self._superproject['revision']:
511 e.setAttribute('revision', self._superproject['revision'])
512 root.appendChild(e)
513
514 if self._contactinfo.bugurl != Wrapper().BUG_URL:
515 root.appendChild(doc.createTextNode(''))
516 e = doc.createElement('contactinfo')
517 e.setAttribute('bugurl', self._contactinfo.bugurl)
518 root.appendChild(e)
519
520 return doc
521
522 def ToDict(self, **kwargs):
523 """Return the current manifest as a dictionary."""
524 # Elements that may only appear once.
525 SINGLE_ELEMENTS = {
526 'notice',
527 'default',
528 'manifest-server',
529 'repo-hooks',
530 'superproject',
531 'contactinfo',
532 }
533 # Elements that may be repeated.
534 MULTI_ELEMENTS = {
535 'remote',
536 'remove-project',
537 'project',
538 'extend-project',
539 'include',
540 # These are children of 'project' nodes.
541 'annotation',
542 'project',
543 'copyfile',
544 'linkfile',
545 }
546
547 doc = self.ToXml(**kwargs)
548 ret = {}
549
550 def append_children(ret, node):
551 for child in node.childNodes:
552 if child.nodeType == xml.dom.Node.ELEMENT_NODE:
553 attrs = child.attributes
554 element = dict((attrs.item(i).localName, attrs.item(i).value)
555 for i in range(attrs.length))
556 if child.nodeName in SINGLE_ELEMENTS:
557 ret[child.nodeName] = element
558 elif child.nodeName in MULTI_ELEMENTS:
559 ret.setdefault(child.nodeName, []).append(element)
560 else:
561 raise ManifestParseError('Unhandled element "%s"' % (child.nodeName,))
562
563 append_children(element, child)
564
565 append_children(ret, doc.firstChild)
566
567 return ret
568
569 def Save(self, fd, **kwargs):
570 """Write the current manifest out to the given file descriptor."""
571 doc = self.ToXml(**kwargs)
375 doc.writexml(fd, '', ' ', '\n', 'UTF-8') 572 doc.writexml(fd, '', ' ', '\n', 'UTF-8')
376 573
377 def _output_manifest_project_extras(self, p, e): 574 def _output_manifest_project_extras(self, p, e):
378 """Manifests can modify e if they support extra project attributes.""" 575 """Manifests can modify e if they support extra project attributes."""
379 pass
380 576
381 @property 577 @property
382 def paths(self): 578 def paths(self):
@@ -404,6 +600,16 @@ class XmlManifest(object):
404 return self._repo_hooks_project 600 return self._repo_hooks_project
405 601
406 @property 602 @property
603 def superproject(self):
604 self._Load()
605 return self._superproject
606
607 @property
608 def contactinfo(self):
609 self._Load()
610 return self._contactinfo
611
612 @property
407 def notice(self): 613 def notice(self):
408 self._Load() 614 self._Load()
409 return self._notice 615 return self._notice
@@ -414,16 +620,45 @@ class XmlManifest(object):
414 return self._manifest_server 620 return self._manifest_server
415 621
416 @property 622 @property
623 def CloneBundle(self):
624 clone_bundle = self.manifestProject.config.GetBoolean('repo.clonebundle')
625 if clone_bundle is None:
626 return False if self.manifestProject.config.GetBoolean('repo.partialclone') else True
627 else:
628 return clone_bundle
629
630 @property
417 def CloneFilter(self): 631 def CloneFilter(self):
418 if self.manifestProject.config.GetBoolean('repo.partialclone'): 632 if self.manifestProject.config.GetBoolean('repo.partialclone'):
419 return self.manifestProject.config.GetString('repo.clonefilter') 633 return self.manifestProject.config.GetString('repo.clonefilter')
420 return None 634 return None
421 635
422 @property 636 @property
637 def PartialCloneExclude(self):
638 exclude = self.manifest.manifestProject.config.GetString(
639 'repo.partialcloneexclude') or ''
640 return set(x.strip() for x in exclude.split(','))
641
642 @property
643 def UseLocalManifests(self):
644 return self._load_local_manifests
645
646 def SetUseLocalManifests(self, value):
647 self._load_local_manifests = value
648
649 @property
650 def HasLocalManifests(self):
651 return self._load_local_manifests and self.local_manifests
652
653 @property
423 def IsMirror(self): 654 def IsMirror(self):
424 return self.manifestProject.config.GetBoolean('repo.mirror') 655 return self.manifestProject.config.GetBoolean('repo.mirror')
425 656
426 @property 657 @property
658 def UseGitWorktrees(self):
659 return self.manifestProject.config.GetBoolean('repo.worktree')
660
661 @property
427 def IsArchive(self): 662 def IsArchive(self):
428 return self.manifestProject.config.GetBoolean('repo.archive') 663 return self.manifestProject.config.GetBoolean('repo.archive')
429 664
@@ -431,6 +666,17 @@ class XmlManifest(object):
431 def HasSubmodules(self): 666 def HasSubmodules(self):
432 return self.manifestProject.config.GetBoolean('repo.submodules') 667 return self.manifestProject.config.GetBoolean('repo.submodules')
433 668
669 def GetDefaultGroupsStr(self):
670 """Returns the default group string for the platform."""
671 return 'default,platform-' + platform.system().lower()
672
673 def GetGroupsStr(self):
674 """Returns the manifest group string that should be synced."""
675 groups = self.manifestProject.config.GetString('manifest.groups')
676 if not groups:
677 groups = self.GetDefaultGroupsStr()
678 return groups
679
434 def _Unload(self): 680 def _Unload(self):
435 self._loaded = False 681 self._loaded = False
436 self._projects = {} 682 self._projects = {}
@@ -438,6 +684,8 @@ class XmlManifest(object):
438 self._remotes = {} 684 self._remotes = {}
439 self._default = None 685 self._default = None
440 self._repo_hooks_project = None 686 self._repo_hooks_project = None
687 self._superproject = {}
688 self._contactinfo = ContactInfo(Wrapper().BUG_URL)
441 self._notice = None 689 self._notice = None
442 self.branch = None 690 self.branch = None
443 self._manifest_server = None 691 self._manifest_server = None
@@ -450,28 +698,24 @@ class XmlManifest(object):
450 b = b[len(R_HEADS):] 698 b = b[len(R_HEADS):]
451 self.branch = b 699 self.branch = b
452 700
701 # The manifestFile was specified by the user which is why we allow include
702 # paths to point anywhere.
453 nodes = [] 703 nodes = []
454 nodes.append(self._ParseManifestXml(self.manifestFile, 704 nodes.append(self._ParseManifestXml(
455 self.manifestProject.worktree)) 705 self.manifestFile, self.manifestProject.worktree,
456 706 restrict_includes=False))
457 if self._load_local_manifests: 707
458 local = os.path.join(self.repodir, LOCAL_MANIFEST_NAME) 708 if self._load_local_manifests and self.local_manifests:
459 if os.path.exists(local):
460 if not self.localManifestWarning:
461 self.localManifestWarning = True
462 print('warning: %s is deprecated; put local manifests '
463 'in `%s` instead' % (LOCAL_MANIFEST_NAME,
464 os.path.join(self.repodir, LOCAL_MANIFESTS_DIR_NAME)),
465 file=sys.stderr)
466 nodes.append(self._ParseManifestXml(local, self.repodir))
467
468 local_dir = os.path.abspath(os.path.join(self.repodir,
469 LOCAL_MANIFESTS_DIR_NAME))
470 try: 709 try:
471 for local_file in sorted(platform_utils.listdir(local_dir)): 710 for local_file in sorted(platform_utils.listdir(self.local_manifests)):
472 if local_file.endswith('.xml'): 711 if local_file.endswith('.xml'):
473 local = os.path.join(local_dir, local_file) 712 local = os.path.join(self.local_manifests, local_file)
474 nodes.append(self._ParseManifestXml(local, self.repodir)) 713 # Since local manifests are entirely managed by the user, allow
714 # them to point anywhere the user wants.
715 nodes.append(self._ParseManifestXml(
716 local, self.repodir,
717 parent_groups=f'{LOCAL_MANIFEST_GROUP_PREFIX}:{local_file[:-4]}',
718 restrict_includes=False))
475 except OSError: 719 except OSError:
476 pass 720 pass
477 721
@@ -489,7 +733,19 @@ class XmlManifest(object):
489 733
490 self._loaded = True 734 self._loaded = True
491 735
492 def _ParseManifestXml(self, path, include_root): 736 def _ParseManifestXml(self, path, include_root, parent_groups='',
737 restrict_includes=True):
738 """Parse a manifest XML and return the computed nodes.
739
740 Args:
741 path: The XML file to read & parse.
742 include_root: The path to interpret include "name"s relative to.
743 parent_groups: The groups to apply to this projects.
744 restrict_includes: Whether to constrain the "name" attribute of includes.
745
746 Returns:
747 List of XML nodes.
748 """
493 try: 749 try:
494 root = xml.dom.minidom.parse(path) 750 root = xml.dom.minidom.parse(path)
495 except (OSError, xml.parsers.expat.ExpatError) as e: 751 except (OSError, xml.parsers.expat.ExpatError) as e:
@@ -508,20 +764,35 @@ class XmlManifest(object):
508 for node in manifest.childNodes: 764 for node in manifest.childNodes:
509 if node.nodeName == 'include': 765 if node.nodeName == 'include':
510 name = self._reqatt(node, 'name') 766 name = self._reqatt(node, 'name')
767 if restrict_includes:
768 msg = self._CheckLocalPath(name)
769 if msg:
770 raise ManifestInvalidPathError(
771 '<include> invalid "name": %s: %s' % (name, msg))
772 include_groups = ''
773 if parent_groups:
774 include_groups = parent_groups
775 if node.hasAttribute('groups'):
776 include_groups = node.getAttribute('groups') + ',' + include_groups
511 fp = os.path.join(include_root, name) 777 fp = os.path.join(include_root, name)
512 if not os.path.isfile(fp): 778 if not os.path.isfile(fp):
513 raise ManifestParseError("include %s doesn't exist or isn't a file" 779 raise ManifestParseError("include [%s/]%s doesn't exist or isn't a file"
514 % (name,)) 780 % (include_root, name))
515 try: 781 try:
516 nodes.extend(self._ParseManifestXml(fp, include_root)) 782 nodes.extend(self._ParseManifestXml(fp, include_root, include_groups))
517 # should isolate this to the exact exception, but that's 783 # should isolate this to the exact exception, but that's
518 # tricky. actual parsing implementation may vary. 784 # tricky. actual parsing implementation may vary.
519 except (KeyboardInterrupt, RuntimeError, SystemExit): 785 except (KeyboardInterrupt, RuntimeError, SystemExit, ManifestParseError):
520 raise 786 raise
521 except Exception as e: 787 except Exception as e:
522 raise ManifestParseError( 788 raise ManifestParseError(
523 "failed parsing included manifest %s: %s" % (name, e)) 789 "failed parsing included manifest %s: %s" % (name, e))
524 else: 790 else:
791 if parent_groups and node.nodeName == 'project':
792 nodeGroups = parent_groups
793 if node.hasAttribute('groups'):
794 nodeGroups = node.getAttribute('groups') + ',' + nodeGroups
795 node.setAttribute('groups', nodeGroups)
525 nodes.append(node) 796 nodes.append(node)
526 return nodes 797 return nodes
527 798
@@ -541,9 +812,10 @@ class XmlManifest(object):
541 for node in itertools.chain(*node_list): 812 for node in itertools.chain(*node_list):
542 if node.nodeName == 'default': 813 if node.nodeName == 'default':
543 new_default = self._ParseDefault(node) 814 new_default = self._ParseDefault(node)
815 emptyDefault = not node.hasAttributes() and not node.hasChildNodes()
544 if self._default is None: 816 if self._default is None:
545 self._default = new_default 817 self._default = new_default
546 elif new_default != self._default: 818 elif not emptyDefault and new_default != self._default:
547 raise ManifestParseError('duplicate default in %s' % 819 raise ManifestParseError('duplicate default in %s' %
548 (self.manifestFile)) 820 (self.manifestFile))
549 821
@@ -582,6 +854,8 @@ class XmlManifest(object):
582 for subproject in project.subprojects: 854 for subproject in project.subprojects:
583 recursively_add_projects(subproject) 855 recursively_add_projects(subproject)
584 856
857 repo_hooks_project = None
858 enabled_repo_hooks = None
585 for node in itertools.chain(*node_list): 859 for node in itertools.chain(*node_list):
586 if node.nodeName == 'project': 860 if node.nodeName == 'project':
587 project = self._ParseProject(node) 861 project = self._ParseProject(node)
@@ -594,61 +868,108 @@ class XmlManifest(object):
594 'project: %s' % name) 868 'project: %s' % name)
595 869
596 path = node.getAttribute('path') 870 path = node.getAttribute('path')
871 dest_path = node.getAttribute('dest-path')
597 groups = node.getAttribute('groups') 872 groups = node.getAttribute('groups')
598 if groups: 873 if groups:
599 groups = self._ParseGroups(groups) 874 groups = self._ParseList(groups)
600 revision = node.getAttribute('revision') 875 revision = node.getAttribute('revision')
876 remote = node.getAttribute('remote')
877 if remote:
878 remote = self._get_remote(node)
601 879
880 named_projects = self._projects[name]
881 if dest_path and not path and len(named_projects) > 1:
882 raise ManifestParseError('extend-project cannot use dest-path when '
883 'matching multiple projects: %s' % name)
602 for p in self._projects[name]: 884 for p in self._projects[name]:
603 if path and p.relpath != path: 885 if path and p.relpath != path:
604 continue 886 continue
605 if groups: 887 if groups:
606 p.groups.extend(groups) 888 p.groups.extend(groups)
607 if revision: 889 if revision:
608 p.revisionExpr = revision 890 p.SetRevision(revision)
609 if node.nodeName == 'repo-hooks': 891
610 # Get the name of the project and the (space-separated) list of enabled. 892 if remote:
611 repo_hooks_project = self._reqatt(node, 'in-project') 893 p.remote = remote.ToRemoteSpec(name)
612 enabled_repo_hooks = self._reqatt(node, 'enabled-list').split() 894
895 if dest_path:
896 del self._paths[p.relpath]
897 relpath, worktree, gitdir, objdir, _ = self.GetProjectPaths(name, dest_path)
898 p.UpdatePaths(relpath, worktree, gitdir, objdir)
899 self._paths[p.relpath] = p
613 900
901 if node.nodeName == 'repo-hooks':
614 # Only one project can be the hooks project 902 # Only one project can be the hooks project
615 if self._repo_hooks_project is not None: 903 if repo_hooks_project is not None:
616 raise ManifestParseError( 904 raise ManifestParseError(
617 'duplicate repo-hooks in %s' % 905 'duplicate repo-hooks in %s' %
618 (self.manifestFile)) 906 (self.manifestFile))
619 907
620 # Store a reference to the Project. 908 # Get the name of the project and the (space-separated) list of enabled.
621 try: 909 repo_hooks_project = self._reqatt(node, 'in-project')
622 repo_hooks_projects = self._projects[repo_hooks_project] 910 enabled_repo_hooks = self._ParseList(self._reqatt(node, 'enabled-list'))
623 except KeyError: 911 if node.nodeName == 'superproject':
624 raise ManifestParseError( 912 name = self._reqatt(node, 'name')
625 'project %s not found for repo-hooks' % 913 # There can only be one superproject.
626 (repo_hooks_project)) 914 if self._superproject.get('name'):
627
628 if len(repo_hooks_projects) != 1:
629 raise ManifestParseError( 915 raise ManifestParseError(
630 'internal error parsing repo-hooks in %s' % 916 'duplicate superproject in %s' %
631 (self.manifestFile)) 917 (self.manifestFile))
632 self._repo_hooks_project = repo_hooks_projects[0] 918 self._superproject['name'] = name
919 remote_name = node.getAttribute('remote')
920 if not remote_name:
921 remote = self._default.remote
922 else:
923 remote = self._get_remote(node)
924 if remote is None:
925 raise ManifestParseError("no remote for superproject %s within %s" %
926 (name, self.manifestFile))
927 self._superproject['remote'] = remote.ToRemoteSpec(name)
928 revision = node.getAttribute('revision') or remote.revision
929 if not revision:
930 revision = self._default.revisionExpr
931 if not revision:
932 raise ManifestParseError('no revision for superproject %s within %s' %
933 (name, self.manifestFile))
934 self._superproject['revision'] = revision
935 if node.nodeName == 'contactinfo':
936 bugurl = self._reqatt(node, 'bugurl')
937 # This element can be repeated, later entries will clobber earlier ones.
938 self._contactinfo = ContactInfo(bugurl)
633 939
634 # Store the enabled hooks in the Project object.
635 self._repo_hooks_project.enabled_repo_hooks = enabled_repo_hooks
636 if node.nodeName == 'remove-project': 940 if node.nodeName == 'remove-project':
637 name = self._reqatt(node, 'name') 941 name = self._reqatt(node, 'name')
638 942
639 if name not in self._projects: 943 if name in self._projects:
944 for p in self._projects[name]:
945 del self._paths[p.relpath]
946 del self._projects[name]
947
948 # If the manifest removes the hooks project, treat it as if it deleted
949 # the repo-hooks element too.
950 if repo_hooks_project == name:
951 repo_hooks_project = None
952 elif not XmlBool(node, 'optional', False):
640 raise ManifestParseError('remove-project element specifies non-existent ' 953 raise ManifestParseError('remove-project element specifies non-existent '
641 'project: %s' % name) 954 'project: %s' % name)
642 955
643 for p in self._projects[name]: 956 # Store repo hooks project information.
644 del self._paths[p.relpath] 957 if repo_hooks_project:
645 del self._projects[name] 958 # Store a reference to the Project.
646 959 try:
647 # If the manifest removes the hooks project, treat it as if it deleted 960 repo_hooks_projects = self._projects[repo_hooks_project]
648 # the repo-hooks element too. 961 except KeyError:
649 if self._repo_hooks_project and (self._repo_hooks_project.name == name): 962 raise ManifestParseError(
650 self._repo_hooks_project = None 963 'project %s not found for repo-hooks' %
964 (repo_hooks_project))
651 965
966 if len(repo_hooks_projects) != 1:
967 raise ManifestParseError(
968 'internal error parsing repo-hooks in %s' %
969 (self.manifestFile))
970 self._repo_hooks_project = repo_hooks_projects[0]
971 # Store the enabled hooks in the Project object.
972 self._repo_hooks_project.enabled_repo_hooks = enabled_repo_hooks
652 973
653 def _AddMetaProjectMirror(self, m): 974 def _AddMetaProjectMirror(self, m):
654 name = None 975 name = None
@@ -676,15 +997,15 @@ class XmlManifest(object):
676 if name not in self._projects: 997 if name not in self._projects:
677 m.PreSync() 998 m.PreSync()
678 gitdir = os.path.join(self.topdir, '%s.git' % name) 999 gitdir = os.path.join(self.topdir, '%s.git' % name)
679 project = Project(manifest = self, 1000 project = Project(manifest=self,
680 name = name, 1001 name=name,
681 remote = remote.ToRemoteSpec(name), 1002 remote=remote.ToRemoteSpec(name),
682 gitdir = gitdir, 1003 gitdir=gitdir,
683 objdir = gitdir, 1004 objdir=gitdir,
684 worktree = None, 1005 worktree=None,
685 relpath = name or None, 1006 relpath=name or None,
686 revisionExpr = m.revisionExpr, 1007 revisionExpr=m.revisionExpr,
687 revisionId = None) 1008 revisionId=None)
688 self._projects[project.name] = [project] 1009 self._projects[project.name] = [project]
689 self._paths[project.relpath] = project 1010 self._paths[project.relpath] = project
690 1011
@@ -707,7 +1028,14 @@ class XmlManifest(object):
707 if revision == '': 1028 if revision == '':
708 revision = None 1029 revision = None
709 manifestUrl = self.manifestProject.config.GetString('remote.origin.url') 1030 manifestUrl = self.manifestProject.config.GetString('remote.origin.url')
710 return _XmlRemote(name, alias, fetch, pushUrl, manifestUrl, review, revision) 1031
1032 remote = _XmlRemote(name, alias, fetch, pushUrl, manifestUrl, review, revision)
1033
1034 for n in node.childNodes:
1035 if n.nodeName == 'annotation':
1036 self._ParseAnnotation(remote, n)
1037
1038 return remote
711 1039
712 def _ParseDefault(self, node): 1040 def _ParseDefault(self, node):
713 """ 1041 """
@@ -722,29 +1050,14 @@ class XmlManifest(object):
722 d.destBranchExpr = node.getAttribute('dest-branch') or None 1050 d.destBranchExpr = node.getAttribute('dest-branch') or None
723 d.upstreamExpr = node.getAttribute('upstream') or None 1051 d.upstreamExpr = node.getAttribute('upstream') or None
724 1052
725 sync_j = node.getAttribute('sync-j') 1053 d.sync_j = XmlInt(node, 'sync-j', 1)
726 if sync_j == '' or sync_j is None: 1054 if d.sync_j <= 0:
727 d.sync_j = 1 1055 raise ManifestParseError('%s: sync-j must be greater than 0, not "%s"' %
728 else: 1056 (self.manifestFile, d.sync_j))
729 d.sync_j = int(sync_j)
730
731 sync_c = node.getAttribute('sync-c')
732 if not sync_c:
733 d.sync_c = False
734 else:
735 d.sync_c = sync_c.lower() in ("yes", "true", "1")
736 1057
737 sync_s = node.getAttribute('sync-s') 1058 d.sync_c = XmlBool(node, 'sync-c', False)
738 if not sync_s: 1059 d.sync_s = XmlBool(node, 'sync-s', False)
739 d.sync_s = False 1060 d.sync_tags = XmlBool(node, 'sync-tags', True)
740 else:
741 d.sync_s = sync_s.lower() in ("yes", "true", "1")
742
743 sync_tags = node.getAttribute('sync-tags')
744 if not sync_tags:
745 d.sync_tags = True
746 else:
747 d.sync_tags = sync_tags.lower() in ("yes", "true", "1")
748 return d 1061 return d
749 1062
750 def _ParseNotice(self, node): 1063 def _ParseNotice(self, node):
@@ -792,11 +1105,15 @@ class XmlManifest(object):
792 def _UnjoinName(self, parent_name, name): 1105 def _UnjoinName(self, parent_name, name):
793 return os.path.relpath(name, parent_name) 1106 return os.path.relpath(name, parent_name)
794 1107
795 def _ParseProject(self, node, parent = None, **extra_proj_attrs): 1108 def _ParseProject(self, node, parent=None, **extra_proj_attrs):
796 """ 1109 """
797 reads a <project> element from the manifest file 1110 reads a <project> element from the manifest file
798 """ 1111 """
799 name = self._reqatt(node, 'name') 1112 name = self._reqatt(node, 'name')
1113 msg = self._CheckLocalPath(name, dir_ok=True)
1114 if msg:
1115 raise ManifestInvalidPathError(
1116 '<project> invalid "name": %s: %s' % (name, msg))
800 if parent: 1117 if parent:
801 name = self._JoinName(parent.name, name) 1118 name = self._JoinName(parent.name, name)
802 1119
@@ -805,55 +1122,34 @@ class XmlManifest(object):
805 remote = self._default.remote 1122 remote = self._default.remote
806 if remote is None: 1123 if remote is None:
807 raise ManifestParseError("no remote for project %s within %s" % 1124 raise ManifestParseError("no remote for project %s within %s" %
808 (name, self.manifestFile)) 1125 (name, self.manifestFile))
809 1126
810 revisionExpr = node.getAttribute('revision') or remote.revision 1127 revisionExpr = node.getAttribute('revision') or remote.revision
811 if not revisionExpr: 1128 if not revisionExpr:
812 revisionExpr = self._default.revisionExpr 1129 revisionExpr = self._default.revisionExpr
813 if not revisionExpr: 1130 if not revisionExpr:
814 raise ManifestParseError("no revision for project %s within %s" % 1131 raise ManifestParseError("no revision for project %s within %s" %
815 (name, self.manifestFile)) 1132 (name, self.manifestFile))
816 1133
817 path = node.getAttribute('path') 1134 path = node.getAttribute('path')
818 if not path: 1135 if not path:
819 path = name 1136 path = name
820 if path.startswith('/'):
821 raise ManifestParseError("project %s path cannot be absolute in %s" %
822 (name, self.manifestFile))
823
824 rebase = node.getAttribute('rebase')
825 if not rebase:
826 rebase = True
827 else: 1137 else:
828 rebase = rebase.lower() in ("yes", "true", "1") 1138 # NB: The "." project is handled specially in Project.Sync_LocalHalf.
829 1139 msg = self._CheckLocalPath(path, dir_ok=True, cwd_dot_ok=True)
830 sync_c = node.getAttribute('sync-c') 1140 if msg:
831 if not sync_c: 1141 raise ManifestInvalidPathError(
832 sync_c = False 1142 '<project> invalid "path": %s: %s' % (path, msg))
833 else: 1143
834 sync_c = sync_c.lower() in ("yes", "true", "1") 1144 rebase = XmlBool(node, 'rebase', True)
835 1145 sync_c = XmlBool(node, 'sync-c', False)
836 sync_s = node.getAttribute('sync-s') 1146 sync_s = XmlBool(node, 'sync-s', self._default.sync_s)
837 if not sync_s: 1147 sync_tags = XmlBool(node, 'sync-tags', self._default.sync_tags)
838 sync_s = self._default.sync_s 1148
839 else: 1149 clone_depth = XmlInt(node, 'clone-depth')
840 sync_s = sync_s.lower() in ("yes", "true", "1") 1150 if clone_depth is not None and clone_depth <= 0:
841 1151 raise ManifestParseError('%s: clone-depth must be greater than 0, not "%s"' %
842 sync_tags = node.getAttribute('sync-tags') 1152 (self.manifestFile, clone_depth))
843 if not sync_tags:
844 sync_tags = self._default.sync_tags
845 else:
846 sync_tags = sync_tags.lower() in ("yes", "true", "1")
847
848 clone_depth = node.getAttribute('clone-depth')
849 if clone_depth:
850 try:
851 clone_depth = int(clone_depth)
852 if clone_depth <= 0:
853 raise ValueError()
854 except ValueError:
855 raise ManifestParseError('invalid clone-depth %s in %s' %
856 (clone_depth, self.manifestFile))
857 1153
858 dest_branch = node.getAttribute('dest-branch') or self._default.destBranchExpr 1154 dest_branch = node.getAttribute('dest-branch') or self._default.destBranchExpr
859 1155
@@ -862,11 +1158,13 @@ class XmlManifest(object):
862 groups = '' 1158 groups = ''
863 if node.hasAttribute('groups'): 1159 if node.hasAttribute('groups'):
864 groups = node.getAttribute('groups') 1160 groups = node.getAttribute('groups')
865 groups = self._ParseGroups(groups) 1161 groups = self._ParseList(groups)
866 1162
867 if parent is None: 1163 if parent is None:
868 relpath, worktree, gitdir, objdir = self.GetProjectPaths(name, path) 1164 relpath, worktree, gitdir, objdir, use_git_worktrees = \
1165 self.GetProjectPaths(name, path)
869 else: 1166 else:
1167 use_git_worktrees = False
870 relpath, worktree, gitdir, objdir = \ 1168 relpath, worktree, gitdir, objdir = \
871 self.GetSubprojectPaths(parent, name, path) 1169 self.GetSubprojectPaths(parent, name, path)
872 1170
@@ -874,27 +1172,28 @@ class XmlManifest(object):
874 groups.extend(set(default_groups).difference(groups)) 1172 groups.extend(set(default_groups).difference(groups))
875 1173
876 if self.IsMirror and node.hasAttribute('force-path'): 1174 if self.IsMirror and node.hasAttribute('force-path'):
877 if node.getAttribute('force-path').lower() in ("yes", "true", "1"): 1175 if XmlBool(node, 'force-path', False):
878 gitdir = os.path.join(self.topdir, '%s.git' % path) 1176 gitdir = os.path.join(self.topdir, '%s.git' % path)
879 1177
880 project = Project(manifest = self, 1178 project = Project(manifest=self,
881 name = name, 1179 name=name,
882 remote = remote.ToRemoteSpec(name), 1180 remote=remote.ToRemoteSpec(name),
883 gitdir = gitdir, 1181 gitdir=gitdir,
884 objdir = objdir, 1182 objdir=objdir,
885 worktree = worktree, 1183 worktree=worktree,
886 relpath = relpath, 1184 relpath=relpath,
887 revisionExpr = revisionExpr, 1185 revisionExpr=revisionExpr,
888 revisionId = None, 1186 revisionId=None,
889 rebase = rebase, 1187 rebase=rebase,
890 groups = groups, 1188 groups=groups,
891 sync_c = sync_c, 1189 sync_c=sync_c,
892 sync_s = sync_s, 1190 sync_s=sync_s,
893 sync_tags = sync_tags, 1191 sync_tags=sync_tags,
894 clone_depth = clone_depth, 1192 clone_depth=clone_depth,
895 upstream = upstream, 1193 upstream=upstream,
896 parent = parent, 1194 parent=parent,
897 dest_branch = dest_branch, 1195 dest_branch=dest_branch,
1196 use_git_worktrees=use_git_worktrees,
898 **extra_proj_attrs) 1197 **extra_proj_attrs)
899 1198
900 for n in node.childNodes: 1199 for n in node.childNodes:
@@ -905,11 +1204,16 @@ class XmlManifest(object):
905 if n.nodeName == 'annotation': 1204 if n.nodeName == 'annotation':
906 self._ParseAnnotation(project, n) 1205 self._ParseAnnotation(project, n)
907 if n.nodeName == 'project': 1206 if n.nodeName == 'project':
908 project.subprojects.append(self._ParseProject(n, parent = project)) 1207 project.subprojects.append(self._ParseProject(n, parent=project))
909 1208
910 return project 1209 return project
911 1210
912 def GetProjectPaths(self, name, path): 1211 def GetProjectPaths(self, name, path):
1212 # The manifest entries might have trailing slashes. Normalize them to avoid
1213 # unexpected filesystem behavior since we do string concatenation below.
1214 path = path.rstrip('/')
1215 name = name.rstrip('/')
1216 use_git_worktrees = False
913 relpath = path 1217 relpath = path
914 if self.IsMirror: 1218 if self.IsMirror:
915 worktree = None 1219 worktree = None
@@ -918,8 +1222,15 @@ class XmlManifest(object):
918 else: 1222 else:
919 worktree = os.path.join(self.topdir, path).replace('\\', '/') 1223 worktree = os.path.join(self.topdir, path).replace('\\', '/')
920 gitdir = os.path.join(self.repodir, 'projects', '%s.git' % path) 1224 gitdir = os.path.join(self.repodir, 'projects', '%s.git' % path)
921 objdir = os.path.join(self.repodir, 'project-objects', '%s.git' % name) 1225 # We allow people to mix git worktrees & non-git worktrees for now.
922 return relpath, worktree, gitdir, objdir 1226 # This allows for in situ migration of repo clients.
1227 if os.path.exists(gitdir) or not self.UseGitWorktrees:
1228 objdir = os.path.join(self.repodir, 'project-objects', '%s.git' % name)
1229 else:
1230 use_git_worktrees = True
1231 gitdir = os.path.join(self.repodir, 'worktrees', '%s.git' % name)
1232 objdir = gitdir
1233 return relpath, worktree, gitdir, objdir, use_git_worktrees
923 1234
924 def GetProjectsWithName(self, name): 1235 def GetProjectsWithName(self, name):
925 return self._projects.get(name, []) 1236 return self._projects.get(name, [])
@@ -934,6 +1245,10 @@ class XmlManifest(object):
934 return os.path.relpath(relpath, parent_relpath) 1245 return os.path.relpath(relpath, parent_relpath)
935 1246
936 def GetSubprojectPaths(self, parent, name, path): 1247 def GetSubprojectPaths(self, parent, name, path):
1248 # The manifest entries might have trailing slashes. Normalize them to avoid
1249 # unexpected filesystem behavior since we do string concatenation below.
1250 path = path.rstrip('/')
1251 name = name.rstrip('/')
937 relpath = self._JoinRelpath(parent.relpath, path) 1252 relpath = self._JoinRelpath(parent.relpath, path)
938 gitdir = os.path.join(parent.gitdir, 'subprojects', '%s.git' % path) 1253 gitdir = os.path.join(parent.gitdir, 'subprojects', '%s.git' % path)
939 objdir = os.path.join(parent.gitdir, 'subproject-objects', '%s.git' % name) 1254 objdir = os.path.join(parent.gitdir, 'subproject-objects', '%s.git' % name)
@@ -943,23 +1258,151 @@ class XmlManifest(object):
943 worktree = os.path.join(parent.worktree, path).replace('\\', '/') 1258 worktree = os.path.join(parent.worktree, path).replace('\\', '/')
944 return relpath, worktree, gitdir, objdir 1259 return relpath, worktree, gitdir, objdir
945 1260
1261 @staticmethod
1262 def _CheckLocalPath(path, dir_ok=False, cwd_dot_ok=False):
1263 """Verify |path| is reasonable for use in filesystem paths.
1264
1265 Used with <copyfile> & <linkfile> & <project> elements.
1266
1267 This only validates the |path| in isolation: it does not check against the
1268 current filesystem state. Thus it is suitable as a first-past in a parser.
1269
1270 It enforces a number of constraints:
1271 * No empty paths.
1272 * No "~" in paths.
1273 * No Unicode codepoints that filesystems might elide when normalizing.
1274 * No relative path components like "." or "..".
1275 * No absolute paths.
1276 * No ".git" or ".repo*" path components.
1277
1278 Args:
1279 path: The path name to validate.
1280 dir_ok: Whether |path| may force a directory (e.g. end in a /).
1281 cwd_dot_ok: Whether |path| may be just ".".
1282
1283 Returns:
1284 None if |path| is OK, a failure message otherwise.
1285 """
1286 if not path:
1287 return 'empty paths not allowed'
1288
1289 if '~' in path:
1290 return '~ not allowed (due to 8.3 filenames on Windows filesystems)'
1291
1292 path_codepoints = set(path)
1293
1294 # Some filesystems (like Apple's HFS+) try to normalize Unicode codepoints
1295 # which means there are alternative names for ".git". Reject paths with
1296 # these in it as there shouldn't be any reasonable need for them here.
1297 # The set of codepoints here was cribbed from jgit's implementation:
1298 # https://eclipse.googlesource.com/jgit/jgit/+/9110037e3e9461ff4dac22fee84ef3694ed57648/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java#884
1299 BAD_CODEPOINTS = {
1300 u'\u200C', # ZERO WIDTH NON-JOINER
1301 u'\u200D', # ZERO WIDTH JOINER
1302 u'\u200E', # LEFT-TO-RIGHT MARK
1303 u'\u200F', # RIGHT-TO-LEFT MARK
1304 u'\u202A', # LEFT-TO-RIGHT EMBEDDING
1305 u'\u202B', # RIGHT-TO-LEFT EMBEDDING
1306 u'\u202C', # POP DIRECTIONAL FORMATTING
1307 u'\u202D', # LEFT-TO-RIGHT OVERRIDE
1308 u'\u202E', # RIGHT-TO-LEFT OVERRIDE
1309 u'\u206A', # INHIBIT SYMMETRIC SWAPPING
1310 u'\u206B', # ACTIVATE SYMMETRIC SWAPPING
1311 u'\u206C', # INHIBIT ARABIC FORM SHAPING
1312 u'\u206D', # ACTIVATE ARABIC FORM SHAPING
1313 u'\u206E', # NATIONAL DIGIT SHAPES
1314 u'\u206F', # NOMINAL DIGIT SHAPES
1315 u'\uFEFF', # ZERO WIDTH NO-BREAK SPACE
1316 }
1317 if BAD_CODEPOINTS & path_codepoints:
1318 # This message is more expansive than reality, but should be fine.
1319 return 'Unicode combining characters not allowed'
1320
1321 # Reject newlines as there shouldn't be any legitmate use for them, they'll
1322 # be confusing to users, and they can easily break tools that expect to be
1323 # able to iterate over newline delimited lists. This even applies to our
1324 # own code like .repo/project.list.
1325 if {'\r', '\n'} & path_codepoints:
1326 return 'Newlines not allowed'
1327
1328 # Assume paths might be used on case-insensitive filesystems.
1329 path = path.lower()
1330
1331 # Split up the path by its components. We can't use os.path.sep exclusively
1332 # as some platforms (like Windows) will convert / to \ and that bypasses all
1333 # our constructed logic here. Especially since manifest authors only use
1334 # / in their paths.
1335 resep = re.compile(r'[/%s]' % re.escape(os.path.sep))
1336 # Strip off trailing slashes as those only produce '' elements, and we use
1337 # parts to look for individual bad components.
1338 parts = resep.split(path.rstrip('/'))
1339
1340 # Some people use src="." to create stable links to projects. Lets allow
1341 # that but reject all other uses of "." to keep things simple.
1342 if not cwd_dot_ok or parts != ['.']:
1343 for part in set(parts):
1344 if part in {'.', '..', '.git'} or part.startswith('.repo'):
1345 return 'bad component: %s' % (part,)
1346
1347 if not dir_ok and resep.match(path[-1]):
1348 return 'dirs not allowed'
1349
1350 # NB: The two abspath checks here are to handle platforms with multiple
1351 # filesystem path styles (e.g. Windows).
1352 norm = os.path.normpath(path)
1353 if (norm == '..' or
1354 (len(norm) >= 3 and norm.startswith('..') and resep.match(norm[0])) or
1355 os.path.isabs(norm) or
1356 norm.startswith('/')):
1357 return 'path cannot be outside'
1358
1359 @classmethod
1360 def _ValidateFilePaths(cls, element, src, dest):
1361 """Verify |src| & |dest| are reasonable for <copyfile> & <linkfile>.
1362
1363 We verify the path independent of any filesystem state as we won't have a
1364 checkout available to compare to. i.e. This is for parsing validation
1365 purposes only.
1366
1367 We'll do full/live sanity checking before we do the actual filesystem
1368 modifications in _CopyFile/_LinkFile/etc...
1369 """
1370 # |dest| is the file we write to or symlink we create.
1371 # It is relative to the top of the repo client checkout.
1372 msg = cls._CheckLocalPath(dest)
1373 if msg:
1374 raise ManifestInvalidPathError(
1375 '<%s> invalid "dest": %s: %s' % (element, dest, msg))
1376
1377 # |src| is the file we read from or path we point to for symlinks.
1378 # It is relative to the top of the git project checkout.
1379 is_linkfile = element == 'linkfile'
1380 msg = cls._CheckLocalPath(src, dir_ok=is_linkfile, cwd_dot_ok=is_linkfile)
1381 if msg:
1382 raise ManifestInvalidPathError(
1383 '<%s> invalid "src": %s: %s' % (element, src, msg))
1384
946 def _ParseCopyFile(self, project, node): 1385 def _ParseCopyFile(self, project, node):
947 src = self._reqatt(node, 'src') 1386 src = self._reqatt(node, 'src')
948 dest = self._reqatt(node, 'dest') 1387 dest = self._reqatt(node, 'dest')
949 if not self.IsMirror: 1388 if not self.IsMirror:
950 # src is project relative; 1389 # src is project relative;
951 # dest is relative to the top of the tree 1390 # dest is relative to the top of the tree.
952 project.AddCopyFile(src, dest, os.path.join(self.topdir, dest)) 1391 # We only validate paths if we actually plan to process them.
1392 self._ValidateFilePaths('copyfile', src, dest)
1393 project.AddCopyFile(src, dest, self.topdir)
953 1394
954 def _ParseLinkFile(self, project, node): 1395 def _ParseLinkFile(self, project, node):
955 src = self._reqatt(node, 'src') 1396 src = self._reqatt(node, 'src')
956 dest = self._reqatt(node, 'dest') 1397 dest = self._reqatt(node, 'dest')
957 if not self.IsMirror: 1398 if not self.IsMirror:
958 # src is project relative; 1399 # src is project relative;
959 # dest is relative to the top of the tree 1400 # dest is relative to the top of the tree.
960 project.AddLinkFile(src, dest, os.path.join(self.topdir, dest)) 1401 # We only validate paths if we actually plan to process them.
1402 self._ValidateFilePaths('linkfile', src, dest)
1403 project.AddLinkFile(src, dest, self.topdir)
961 1404
962 def _ParseAnnotation(self, project, node): 1405 def _ParseAnnotation(self, element, node):
963 name = self._reqatt(node, 'name') 1406 name = self._reqatt(node, 'name')
964 value = self._reqatt(node, 'value') 1407 value = self._reqatt(node, 'value')
965 try: 1408 try:
@@ -968,8 +1411,8 @@ class XmlManifest(object):
968 keep = "true" 1411 keep = "true"
969 if keep != "true" and keep != "false": 1412 if keep != "true" and keep != "false":
970 raise ManifestParseError('optional "keep" attribute must be ' 1413 raise ManifestParseError('optional "keep" attribute must be '
971 '"true" or "false"') 1414 '"true" or "false"')
972 project.AddAnnotation(name, value, keep) 1415 element.AddAnnotation(name, value, keep)
973 1416
974 def _get_remote(self, node): 1417 def _get_remote(self, node):
975 name = node.getAttribute('remote') 1418 name = node.getAttribute('remote')
@@ -979,7 +1422,7 @@ class XmlManifest(object):
979 v = self._remotes.get(name) 1422 v = self._remotes.get(name)
980 if not v: 1423 if not v:
981 raise ManifestParseError("remote %s not defined in %s" % 1424 raise ManifestParseError("remote %s not defined in %s" %
982 (name, self.manifestFile)) 1425 (name, self.manifestFile))
983 return v 1426 return v
984 1427
985 def _reqatt(self, node, attname): 1428 def _reqatt(self, node, attname):
@@ -989,7 +1432,7 @@ class XmlManifest(object):
989 v = node.getAttribute(attname) 1432 v = node.getAttribute(attname)
990 if not v: 1433 if not v:
991 raise ManifestParseError("no %s in <%s> within %s" % 1434 raise ManifestParseError("no %s in <%s> within %s" %
992 (attname, node.nodeName, self.manifestFile)) 1435 (attname, node.nodeName, self.manifestFile))
993 return v 1436 return v
994 1437
995 def projectsDiff(self, manifest): 1438 def projectsDiff(self, manifest):
@@ -1007,7 +1450,7 @@ class XmlManifest(object):
1007 diff = {'added': [], 'removed': [], 'changed': [], 'unreachable': []} 1450 diff = {'added': [], 'removed': [], 'changed': [], 'unreachable': []}
1008 1451
1009 for proj in fromKeys: 1452 for proj in fromKeys:
1010 if not proj in toKeys: 1453 if proj not in toKeys:
1011 diff['removed'].append(fromProjects[proj]) 1454 diff['removed'].append(fromProjects[proj])
1012 else: 1455 else:
1013 fromProj = fromProjects[proj] 1456 fromProj = fromProjects[proj]
@@ -1029,19 +1472,11 @@ class XmlManifest(object):
1029 1472
1030 1473
1031class GitcManifest(XmlManifest): 1474class GitcManifest(XmlManifest):
1475 """Parser for GitC (git-in-the-cloud) manifests."""
1032 1476
1033 def __init__(self, repodir, gitc_client_name): 1477 def _ParseProject(self, node, parent=None):
1034 """Initialize the GitcManifest object."""
1035 super(GitcManifest, self).__init__(repodir)
1036 self.isGitcClient = True
1037 self.gitc_client_name = gitc_client_name
1038 self.gitc_client_dir = os.path.join(gitc_utils.get_gitc_manifest_dir(),
1039 gitc_client_name)
1040 self.manifestFile = os.path.join(self.gitc_client_dir, '.manifest')
1041
1042 def _ParseProject(self, node, parent = None):
1043 """Override _ParseProject and add support for GITC specific attributes.""" 1478 """Override _ParseProject and add support for GITC specific attributes."""
1044 return super(GitcManifest, self)._ParseProject( 1479 return super()._ParseProject(
1045 node, parent=parent, old_revision=node.getAttribute('old-revision')) 1480 node, parent=parent, old_revision=node.getAttribute('old-revision'))
1046 1481
1047 def _output_manifest_project_extras(self, p, e): 1482 def _output_manifest_project_extras(self, p, e):
@@ -1049,3 +1484,36 @@ class GitcManifest(XmlManifest):
1049 if p.old_revision: 1484 if p.old_revision:
1050 e.setAttribute('old-revision', str(p.old_revision)) 1485 e.setAttribute('old-revision', str(p.old_revision))
1051 1486
1487
1488class RepoClient(XmlManifest):
1489 """Manages a repo client checkout."""
1490
1491 def __init__(self, repodir, manifest_file=None):
1492 self.isGitcClient = False
1493
1494 if os.path.exists(os.path.join(repodir, LOCAL_MANIFEST_NAME)):
1495 print('error: %s is not supported; put local manifests in `%s` instead' %
1496 (LOCAL_MANIFEST_NAME, os.path.join(repodir, LOCAL_MANIFESTS_DIR_NAME)),
1497 file=sys.stderr)
1498 sys.exit(1)
1499
1500 if manifest_file is None:
1501 manifest_file = os.path.join(repodir, MANIFEST_FILE_NAME)
1502 local_manifests = os.path.abspath(os.path.join(repodir, LOCAL_MANIFESTS_DIR_NAME))
1503 super().__init__(repodir, manifest_file, local_manifests)
1504
1505 # TODO: Completely separate manifest logic out of the client.
1506 self.manifest = self
1507
1508
1509class GitcClient(RepoClient, GitcManifest):
1510 """Manages a GitC client checkout."""
1511
1512 def __init__(self, repodir, gitc_client_name):
1513 """Initialize the GitcManifest object."""
1514 self.gitc_client_name = gitc_client_name
1515 self.gitc_client_dir = os.path.join(gitc_utils.get_gitc_manifest_dir(),
1516 gitc_client_name)
1517
1518 super().__init__(repodir, os.path.join(self.gitc_client_dir, '.manifest'))
1519 self.isGitcClient = True
diff --git a/pager.py b/pager.py
index 221baf3c..352923d9 100644
--- a/pager.py
+++ b/pager.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,7 +12,6 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function
18import os 15import os
19import select 16import select
20import subprocess 17import subprocess
@@ -27,6 +24,7 @@ pager_process = None
27old_stdout = None 24old_stdout = None
28old_stderr = None 25old_stderr = None
29 26
27
30def RunPager(globalConfig): 28def RunPager(globalConfig):
31 if not os.isatty(0) or not os.isatty(1): 29 if not os.isatty(0) or not os.isatty(1):
32 return 30 return
@@ -35,33 +33,37 @@ def RunPager(globalConfig):
35 return 33 return
36 34
37 if platform_utils.isWindows(): 35 if platform_utils.isWindows():
38 _PipePager(pager); 36 _PipePager(pager)
39 else: 37 else:
40 _ForkPager(pager) 38 _ForkPager(pager)
41 39
40
42def TerminatePager(): 41def TerminatePager():
43 global pager_process, old_stdout, old_stderr 42 global pager_process, old_stdout, old_stderr
44 if pager_process: 43 if pager_process:
45 sys.stdout.flush() 44 sys.stdout.flush()
46 sys.stderr.flush() 45 sys.stderr.flush()
47 pager_process.stdin.close() 46 pager_process.stdin.close()
48 pager_process.wait(); 47 pager_process.wait()
49 pager_process = None 48 pager_process = None
50 # Restore initial stdout/err in case there is more output in this process 49 # Restore initial stdout/err in case there is more output in this process
51 # after shutting down the pager process 50 # after shutting down the pager process
52 sys.stdout = old_stdout 51 sys.stdout = old_stdout
53 sys.stderr = old_stderr 52 sys.stderr = old_stderr
54 53
54
55def _PipePager(pager): 55def _PipePager(pager):
56 global pager_process, old_stdout, old_stderr 56 global pager_process, old_stdout, old_stderr
57 assert pager_process is None, "Only one active pager process at a time" 57 assert pager_process is None, "Only one active pager process at a time"
58 # Create pager process, piping stdout/err into its stdin 58 # Create pager process, piping stdout/err into its stdin
59 pager_process = subprocess.Popen([pager], stdin=subprocess.PIPE, stdout=sys.stdout, stderr=sys.stderr) 59 pager_process = subprocess.Popen([pager], stdin=subprocess.PIPE, stdout=sys.stdout,
60 stderr=sys.stderr)
60 old_stdout = sys.stdout 61 old_stdout = sys.stdout
61 old_stderr = sys.stderr 62 old_stderr = sys.stderr
62 sys.stdout = pager_process.stdin 63 sys.stdout = pager_process.stdin
63 sys.stderr = pager_process.stdin 64 sys.stderr = pager_process.stdin
64 65
66
65def _ForkPager(pager): 67def _ForkPager(pager):
66 global active 68 global active
67 # This process turns into the pager; a child it forks will 69 # This process turns into the pager; a child it forks will
@@ -88,6 +90,7 @@ def _ForkPager(pager):
88 print("fatal: cannot start pager '%s'" % pager, file=sys.stderr) 90 print("fatal: cannot start pager '%s'" % pager, file=sys.stderr)
89 sys.exit(255) 91 sys.exit(255)
90 92
93
91def _SelectPager(globalConfig): 94def _SelectPager(globalConfig):
92 try: 95 try:
93 return os.environ['GIT_PAGER'] 96 return os.environ['GIT_PAGER']
@@ -105,6 +108,7 @@ def _SelectPager(globalConfig):
105 108
106 return 'less' 109 return 'less'
107 110
111
108def _BecomePager(pager): 112def _BecomePager(pager):
109 # Delaying execution of the pager until we have output 113 # Delaying execution of the pager until we have output
110 # ready works around a long-standing bug in popularly 114 # ready works around a long-standing bug in popularly
diff --git a/platform_utils.py b/platform_utils.py
index 06ef9b18..0203249a 100644
--- a/platform_utils.py
+++ b/platform_utils.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2016 The Android Open Source Project 1# Copyright (C) 2016 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,18 +15,9 @@
17import errno 15import errno
18import os 16import os
19import platform 17import platform
20import select
21import shutil 18import shutil
22import stat 19import stat
23 20
24from pyversion import is_python3
25if is_python3():
26 from queue import Queue
27else:
28 from Queue import Queue
29
30from threading import Thread
31
32 21
33def isWindows(): 22def isWindows():
34 """ Returns True when running with the native port of Python for Windows, 23 """ Returns True when running with the native port of Python for Windows,
@@ -39,145 +28,6 @@ def isWindows():
39 return platform.system() == "Windows" 28 return platform.system() == "Windows"
40 29
41 30
42class FileDescriptorStreams(object):
43 """ Platform agnostic abstraction enabling non-blocking I/O over a
44 collection of file descriptors. This abstraction is required because
45 fctnl(os.O_NONBLOCK) is not supported on Windows.
46 """
47 @classmethod
48 def create(cls):
49 """ Factory method: instantiates the concrete class according to the
50 current platform.
51 """
52 if isWindows():
53 return _FileDescriptorStreamsThreads()
54 else:
55 return _FileDescriptorStreamsNonBlocking()
56
57 def __init__(self):
58 self.streams = []
59
60 def add(self, fd, dest, std_name):
61 """ Wraps an existing file descriptor as a stream.
62 """
63 self.streams.append(self._create_stream(fd, dest, std_name))
64
65 def remove(self, stream):
66 """ Removes a stream, when done with it.
67 """
68 self.streams.remove(stream)
69
70 @property
71 def is_done(self):
72 """ Returns True when all streams have been processed.
73 """
74 return len(self.streams) == 0
75
76 def select(self):
77 """ Returns the set of streams that have data available to read.
78 The returned streams each expose a read() and a close() method.
79 When done with a stream, call the remove(stream) method.
80 """
81 raise NotImplementedError
82
83 def _create_stream(self, fd, dest, std_name):
84 """ Creates a new stream wrapping an existing file descriptor.
85 """
86 raise NotImplementedError
87
88
89class _FileDescriptorStreamsNonBlocking(FileDescriptorStreams):
90 """ Implementation of FileDescriptorStreams for platforms that support
91 non blocking I/O.
92 """
93 class Stream(object):
94 """ Encapsulates a file descriptor """
95 def __init__(self, fd, dest, std_name):
96 self.fd = fd
97 self.dest = dest
98 self.std_name = std_name
99 self.set_non_blocking()
100
101 def set_non_blocking(self):
102 import fcntl
103 flags = fcntl.fcntl(self.fd, fcntl.F_GETFL)
104 fcntl.fcntl(self.fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
105
106 def fileno(self):
107 return self.fd.fileno()
108
109 def read(self):
110 return self.fd.read(4096)
111
112 def close(self):
113 self.fd.close()
114
115 def _create_stream(self, fd, dest, std_name):
116 return self.Stream(fd, dest, std_name)
117
118 def select(self):
119 ready_streams, _, _ = select.select(self.streams, [], [])
120 return ready_streams
121
122
123class _FileDescriptorStreamsThreads(FileDescriptorStreams):
124 """ Implementation of FileDescriptorStreams for platforms that don't support
125 non blocking I/O. This implementation requires creating threads issuing
126 blocking read operations on file descriptors.
127 """
128 def __init__(self):
129 super(_FileDescriptorStreamsThreads, self).__init__()
130 # The queue is shared accross all threads so we can simulate the
131 # behavior of the select() function
132 self.queue = Queue(10) # Limit incoming data from streams
133
134 def _create_stream(self, fd, dest, std_name):
135 return self.Stream(fd, dest, std_name, self.queue)
136
137 def select(self):
138 # Return only one stream at a time, as it is the most straighforward
139 # thing to do and it is compatible with the select() function.
140 item = self.queue.get()
141 stream = item.stream
142 stream.data = item.data
143 return [stream]
144
145 class QueueItem(object):
146 """ Item put in the shared queue """
147 def __init__(self, stream, data):
148 self.stream = stream
149 self.data = data
150
151 class Stream(object):
152 """ Encapsulates a file descriptor """
153 def __init__(self, fd, dest, std_name, queue):
154 self.fd = fd
155 self.dest = dest
156 self.std_name = std_name
157 self.queue = queue
158 self.data = None
159 self.thread = Thread(target=self.read_to_queue)
160 self.thread.daemon = True
161 self.thread.start()
162
163 def close(self):
164 self.fd.close()
165
166 def read(self):
167 data = self.data
168 self.data = None
169 return data
170
171 def read_to_queue(self):
172 """ The thread function: reads everything from the file descriptor into
173 the shared queue and terminates when reaching EOF.
174 """
175 for line in iter(self.fd.readline, b''):
176 self.queue.put(_FileDescriptorStreamsThreads.QueueItem(self, line))
177 self.fd.close()
178 self.queue.put(_FileDescriptorStreamsThreads.QueueItem(self, None))
179
180
181def symlink(source, link_name): 31def symlink(source, link_name):
182 """Creates a symbolic link pointing to source named link_name. 32 """Creates a symbolic link pointing to source named link_name.
183 Note: On Windows, source must exist on disk, as the implementation needs 33 Note: On Windows, source must exist on disk, as the implementation needs
@@ -274,31 +124,30 @@ def rename(src, dst):
274 else: 124 else:
275 raise 125 raise
276 else: 126 else:
277 os.rename(src, dst) 127 shutil.move(src, dst)
278 128
279 129
280def remove(path): 130def remove(path, missing_ok=False):
281 """Remove (delete) the file path. This is a replacement for os.remove that 131 """Remove (delete) the file path. This is a replacement for os.remove that
282 allows deleting read-only files on Windows, with support for long paths and 132 allows deleting read-only files on Windows, with support for long paths and
283 for deleting directory symbolic links. 133 for deleting directory symbolic links.
284 134
285 Availability: Unix, Windows.""" 135 Availability: Unix, Windows."""
286 if isWindows(): 136 longpath = _makelongpath(path) if isWindows() else path
287 longpath = _makelongpath(path) 137 try:
288 try: 138 os.remove(longpath)
289 os.remove(longpath) 139 except OSError as e:
290 except OSError as e: 140 if e.errno == errno.EACCES:
291 if e.errno == errno.EACCES: 141 os.chmod(longpath, stat.S_IWRITE)
292 os.chmod(longpath, stat.S_IWRITE) 142 # Directory symbolic links must be deleted with 'rmdir'.
293 # Directory symbolic links must be deleted with 'rmdir'. 143 if islink(longpath) and isdir(longpath):
294 if islink(longpath) and isdir(longpath): 144 os.rmdir(longpath)
295 os.rmdir(longpath)
296 else:
297 os.remove(longpath)
298 else: 145 else:
299 raise 146 os.remove(longpath)
300 else: 147 elif missing_ok and e.errno == errno.ENOENT:
301 os.remove(path) 148 pass
149 else:
150 raise
302 151
303 152
304def walk(top, topdown=True, onerror=None, followlinks=False): 153def walk(top, topdown=True, onerror=None, followlinks=False):
diff --git a/platform_utils_win32.py b/platform_utils_win32.py
index e20755a4..bf916d47 100644
--- a/platform_utils_win32.py
+++ b/platform_utils_win32.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2016 The Android Open Source Project 1# Copyright (C) 2016 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,16 +14,13 @@
16 14
17import errno 15import errno
18 16
19from pyversion import is_python3
20from ctypes import WinDLL, get_last_error, FormatError, WinError, addressof 17from ctypes import WinDLL, get_last_error, FormatError, WinError, addressof
21from ctypes import c_buffer 18from ctypes import c_buffer, c_ubyte, Structure, Union, byref
22from ctypes.wintypes import BOOL, BOOLEAN, LPCWSTR, DWORD, HANDLE, POINTER, c_ubyte 19from ctypes.wintypes import BOOL, BOOLEAN, LPCWSTR, DWORD, HANDLE
23from ctypes.wintypes import WCHAR, USHORT, LPVOID, Structure, Union, ULONG 20from ctypes.wintypes import WCHAR, USHORT, LPVOID, ULONG, LPDWORD
24from ctypes.wintypes import byref
25 21
26kernel32 = WinDLL('kernel32', use_last_error=True) 22kernel32 = WinDLL('kernel32', use_last_error=True)
27 23
28LPDWORD = POINTER(DWORD)
29UCHAR = c_ubyte 24UCHAR = c_ubyte
30 25
31# Win32 error codes 26# Win32 error codes
@@ -147,7 +142,8 @@ def create_dirsymlink(source, link_name):
147 142
148 143
149def _create_symlink(source, link_name, dwFlags): 144def _create_symlink(source, link_name, dwFlags):
150 if not CreateSymbolicLinkW(link_name, source, dwFlags | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE): 145 if not CreateSymbolicLinkW(link_name, source,
146 dwFlags | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE):
151 # See https://github.com/golang/go/pull/24307/files#diff-b87bc12e4da2497308f9ef746086e4f0 147 # See https://github.com/golang/go/pull/24307/files#diff-b87bc12e4da2497308f9ef746086e4f0
152 # "the unprivileged create flag is unsupported below Windows 10 (1703, v10.0.14972). 148 # "the unprivileged create flag is unsupported below Windows 10 (1703, v10.0.14972).
153 # retry without it." 149 # retry without it."
@@ -198,26 +194,15 @@ def readlink(path):
198 'Error reading symbolic link \"%s\"'.format(path)) 194 'Error reading symbolic link \"%s\"'.format(path))
199 rdb = REPARSE_DATA_BUFFER.from_buffer(target_buffer) 195 rdb = REPARSE_DATA_BUFFER.from_buffer(target_buffer)
200 if rdb.ReparseTag == IO_REPARSE_TAG_SYMLINK: 196 if rdb.ReparseTag == IO_REPARSE_TAG_SYMLINK:
201 return _preserve_encoding(path, rdb.SymbolicLinkReparseBuffer.PrintName) 197 return rdb.SymbolicLinkReparseBuffer.PrintName
202 elif rdb.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT: 198 elif rdb.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT:
203 return _preserve_encoding(path, rdb.MountPointReparseBuffer.PrintName) 199 return rdb.MountPointReparseBuffer.PrintName
204 # Unsupported reparse point type 200 # Unsupported reparse point type
205 _raise_winerror( 201 _raise_winerror(
206 ERROR_NOT_SUPPORTED, 202 ERROR_NOT_SUPPORTED,
207 'Error reading symbolic link \"%s\"'.format(path)) 203 'Error reading symbolic link \"%s\"'.format(path))
208 204
209 205
210def _preserve_encoding(source, target):
211 """Ensures target is the same string type (i.e. unicode or str) as source."""
212
213 if is_python3():
214 return target
215
216 if isinstance(source, unicode):
217 return unicode(target)
218 return str(target)
219
220
221def _raise_winerror(code, error_desc): 206def _raise_winerror(code, error_desc):
222 win_error_desc = FormatError(code).strip() 207 win_error_desc = FormatError(code).strip()
223 error_desc = "%s: %s".format(error_desc, win_error_desc) 208 error_desc = "%s: %s".format(error_desc, win_error_desc)
diff --git a/progress.py b/progress.py
index d2ed4bae..43c7ad21 100644
--- a/progress.py
+++ b/progress.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2009 The Android Open Source Project 1# Copyright (C) 2009 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,18 +24,53 @@ _NOT_TTY = not os.isatty(2)
26# column 0. 24# column 0.
27CSI_ERASE_LINE = '\x1b[2K' 25CSI_ERASE_LINE = '\x1b[2K'
28 26
27
28def duration_str(total):
29 """A less noisy timedelta.__str__.
30
31 The default timedelta stringification contains a lot of leading zeros and
32 uses microsecond resolution. This makes for noisy output.
33 """
34 hours, rem = divmod(total, 3600)
35 mins, secs = divmod(rem, 60)
36 ret = '%.3fs' % (secs,)
37 if mins:
38 ret = '%im%s' % (mins, ret)
39 if hours:
40 ret = '%ih%s' % (hours, ret)
41 return ret
42
43
29class Progress(object): 44class Progress(object):
30 def __init__(self, title, total=0, units='', print_newline=False, 45 def __init__(self, title, total=0, units='', print_newline=False, delay=True,
31 always_print_percentage=False): 46 quiet=False):
32 self._title = title 47 self._title = title
33 self._total = total 48 self._total = total
34 self._done = 0 49 self._done = 0
35 self._lastp = -1
36 self._start = time() 50 self._start = time()
37 self._show = False 51 self._show = not delay
38 self._units = units 52 self._units = units
39 self._print_newline = print_newline 53 self._print_newline = print_newline
40 self._always_print_percentage = always_print_percentage 54 # Only show the active jobs section if we run more than one in parallel.
55 self._show_jobs = False
56 self._active = 0
57
58 # When quiet, never show any output. It's a bit hacky, but reusing the
59 # existing logic that delays initial output keeps the rest of the class
60 # clean. Basically we set the start time to years in the future.
61 if quiet:
62 self._show = False
63 self._start += 2**32
64
65 def start(self, name):
66 self._active += 1
67 if not self._show_jobs:
68 self._show_jobs = self._active > 1
69 self.update(inc=0, msg='started ' + name)
70
71 def finish(self, name):
72 self.update(msg='finished ' + name)
73 self._active -= 1
41 74
42 def update(self, inc=1, msg=''): 75 def update(self, inc=1, msg=''):
43 self._done += inc 76 self._done += inc
@@ -53,41 +86,46 @@ class Progress(object):
53 86
54 if self._total <= 0: 87 if self._total <= 0:
55 sys.stderr.write('%s\r%s: %d,' % ( 88 sys.stderr.write('%s\r%s: %d,' % (
56 CSI_ERASE_LINE, 89 CSI_ERASE_LINE,
57 self._title, 90 self._title,
58 self._done)) 91 self._done))
59 sys.stderr.flush() 92 sys.stderr.flush()
60 else: 93 else:
61 p = (100 * self._done) / self._total 94 p = (100 * self._done) / self._total
62 95 if self._show_jobs:
63 if self._lastp != p or self._always_print_percentage: 96 jobs = '[%d job%s] ' % (self._active, 's' if self._active > 1 else '')
64 self._lastp = p 97 else:
65 sys.stderr.write('%s\r%s: %3d%% (%d%s/%d%s)%s%s%s' % ( 98 jobs = ''
99 sys.stderr.write('%s\r%s: %2d%% %s(%d%s/%d%s)%s%s%s' % (
66 CSI_ERASE_LINE, 100 CSI_ERASE_LINE,
67 self._title, 101 self._title,
68 p, 102 p,
103 jobs,
69 self._done, self._units, 104 self._done, self._units,
70 self._total, self._units, 105 self._total, self._units,
71 ' ' if msg else '', msg, 106 ' ' if msg else '', msg,
72 "\n" if self._print_newline else "")) 107 '\n' if self._print_newline else ''))
73 sys.stderr.flush() 108 sys.stderr.flush()
74 109
75 def end(self): 110 def end(self):
76 if _NOT_TTY or IsTrace() or not self._show: 111 if _NOT_TTY or IsTrace() or not self._show:
77 return 112 return
78 113
114 duration = duration_str(time() - self._start)
79 if self._total <= 0: 115 if self._total <= 0:
80 sys.stderr.write('%s\r%s: %d, done.\n' % ( 116 sys.stderr.write('%s\r%s: %d, done in %s\n' % (
81 CSI_ERASE_LINE, 117 CSI_ERASE_LINE,
82 self._title, 118 self._title,
83 self._done)) 119 self._done,
120 duration))
84 sys.stderr.flush() 121 sys.stderr.flush()
85 else: 122 else:
86 p = (100 * self._done) / self._total 123 p = (100 * self._done) / self._total
87 sys.stderr.write('%s\r%s: %3d%% (%d%s/%d%s), done.\n' % ( 124 sys.stderr.write('%s\r%s: %3d%% (%d%s/%d%s), done in %s\n' % (
88 CSI_ERASE_LINE, 125 CSI_ERASE_LINE,
89 self._title, 126 self._title,
90 p, 127 p,
91 self._done, self._units, 128 self._done, self._units,
92 self._total, self._units)) 129 self._total, self._units,
130 duration))
93 sys.stderr.flush() 131 sys.stderr.flush()
diff --git a/project.py b/project.py
index 8fdacc65..5b26b64c 100644
--- a/project.py
+++ b/project.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,11 +12,9 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function
18import errno 15import errno
19import filecmp 16import filecmp
20import glob 17import glob
21import json
22import os 18import os
23import random 19import random
24import re 20import re
@@ -29,36 +25,33 @@ import sys
29import tarfile 25import tarfile
30import tempfile 26import tempfile
31import time 27import time
32import traceback 28import urllib.parse
33 29
34from color import Coloring 30from color import Coloring
35from git_command import GitCommand, git_require 31from git_command import GitCommand, git_require
36from git_config import GitConfig, IsId, GetSchemeFromUrl, GetUrlCookieFile, \ 32from git_config import GitConfig, IsId, GetSchemeFromUrl, GetUrlCookieFile, \
37 ID_RE 33 ID_RE
38from error import GitError, HookError, UploadError, DownloadError 34from error import GitError, UploadError, DownloadError
39from error import ManifestInvalidRevisionError 35from error import ManifestInvalidRevisionError, ManifestInvalidPathError
40from error import NoManifestException 36from error import NoManifestException
41import platform_utils 37import platform_utils
42import progress 38import progress
43from repo_trace import IsTrace, Trace 39from repo_trace import IsTrace, Trace
44 40
45from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M 41from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M, R_WORKTREE_M
42
46 43
47from pyversion import is_python3 44# Maximum sleep time allowed during retries.
48if is_python3(): 45MAXIMUM_RETRY_SLEEP_SEC = 3600.0
49 import urllib.parse 46# +-10% random jitter is added to each Fetches retry sleep duration.
50else: 47RETRY_JITTER_PERCENT = 0.1
51 import imp
52 import urlparse
53 urllib = imp.new_module('urllib')
54 urllib.parse = urlparse
55 input = raw_input
56 48
57 49
58def _lwrite(path, content): 50def _lwrite(path, content):
59 lock = '%s.lock' % path 51 lock = '%s.lock' % path
60 52
61 with open(lock, 'w') as fd: 53 # Maintain Unix line endings on all OS's to match git behavior.
54 with open(lock, 'w', newline='\n') as fd:
62 fd.write(content) 55 fd.write(content)
63 56
64 try: 57 try:
@@ -85,6 +78,7 @@ def not_rev(r):
85def sq(r): 78def sq(r):
86 return "'" + r.replace("'", "'\''") + "'" 79 return "'" + r.replace("'", "'\''") + "'"
87 80
81
88_project_hook_list = None 82_project_hook_list = None
89 83
90 84
@@ -197,18 +191,22 @@ class ReviewableBranch(object):
197 return self._base_exists 191 return self._base_exists
198 192
199 def UploadForReview(self, people, 193 def UploadForReview(self, people,
194 dryrun=False,
200 auto_topic=False, 195 auto_topic=False,
201 draft=False, 196 hashtags=(),
197 labels=(),
202 private=False, 198 private=False,
203 notify=None, 199 notify=None,
204 wip=False, 200 wip=False,
205 dest_branch=None, 201 dest_branch=None,
206 validate_certs=True, 202 validate_certs=True,
207 push_options=None): 203 push_options=None):
208 self.project.UploadForReview(self.name, 204 self.project.UploadForReview(branch=self.name,
209 people, 205 people=people,
206 dryrun=dryrun,
210 auto_topic=auto_topic, 207 auto_topic=auto_topic,
211 draft=draft, 208 hashtags=hashtags,
209 labels=labels,
212 private=private, 210 private=private,
213 notify=notify, 211 notify=notify,
214 wip=wip, 212 wip=wip,
@@ -234,7 +232,7 @@ class ReviewableBranch(object):
234class StatusColoring(Coloring): 232class StatusColoring(Coloring):
235 233
236 def __init__(self, config): 234 def __init__(self, config):
237 Coloring.__init__(self, config, 'status') 235 super().__init__(config, 'status')
238 self.project = self.printer('header', attr='bold') 236 self.project = self.printer('header', attr='bold')
239 self.branch = self.printer('header', attr='bold') 237 self.branch = self.printer('header', attr='bold')
240 self.nobranch = self.printer('nobranch', fg='red') 238 self.nobranch = self.printer('nobranch', fg='red')
@@ -248,30 +246,104 @@ class StatusColoring(Coloring):
248class DiffColoring(Coloring): 246class DiffColoring(Coloring):
249 247
250 def __init__(self, config): 248 def __init__(self, config):
251 Coloring.__init__(self, config, 'diff') 249 super().__init__(config, 'diff')
252 self.project = self.printer('header', attr='bold') 250 self.project = self.printer('header', attr='bold')
253 self.fail = self.printer('fail', fg='red') 251 self.fail = self.printer('fail', fg='red')
254 252
255 253
256class _Annotation(object): 254class Annotation(object):
257 255
258 def __init__(self, name, value, keep): 256 def __init__(self, name, value, keep):
259 self.name = name 257 self.name = name
260 self.value = value 258 self.value = value
261 self.keep = keep 259 self.keep = keep
262 260
261 def __eq__(self, other):
262 if not isinstance(other, Annotation):
263 return False
264 return self.__dict__ == other.__dict__
265
266 def __lt__(self, other):
267 # This exists just so that lists of Annotation objects can be sorted, for
268 # use in comparisons.
269 if not isinstance(other, Annotation):
270 raise ValueError('comparison is not between two Annotation objects')
271 if self.name == other.name:
272 if self.value == other.value:
273 return self.keep < other.keep
274 return self.value < other.value
275 return self.name < other.name
276
277
278def _SafeExpandPath(base, subpath, skipfinal=False):
279 """Make sure |subpath| is completely safe under |base|.
280
281 We make sure no intermediate symlinks are traversed, and that the final path
282 is not a special file (e.g. not a socket or fifo).
283
284 NB: We rely on a number of paths already being filtered out while parsing the
285 manifest. See the validation logic in manifest_xml.py for more details.
286 """
287 # Split up the path by its components. We can't use os.path.sep exclusively
288 # as some platforms (like Windows) will convert / to \ and that bypasses all
289 # our constructed logic here. Especially since manifest authors only use
290 # / in their paths.
291 resep = re.compile(r'[/%s]' % re.escape(os.path.sep))
292 components = resep.split(subpath)
293 if skipfinal:
294 # Whether the caller handles the final component itself.
295 finalpart = components.pop()
296
297 path = base
298 for part in components:
299 if part in {'.', '..'}:
300 raise ManifestInvalidPathError(
301 '%s: "%s" not allowed in paths' % (subpath, part))
302
303 path = os.path.join(path, part)
304 if platform_utils.islink(path):
305 raise ManifestInvalidPathError(
306 '%s: traversing symlinks not allow' % (path,))
307
308 if os.path.exists(path):
309 if not os.path.isfile(path) and not platform_utils.isdir(path):
310 raise ManifestInvalidPathError(
311 '%s: only regular files & directories allowed' % (path,))
312
313 if skipfinal:
314 path = os.path.join(path, finalpart)
315
316 return path
317
263 318
264class _CopyFile(object): 319class _CopyFile(object):
320 """Container for <copyfile> manifest element."""
321
322 def __init__(self, git_worktree, src, topdir, dest):
323 """Register a <copyfile> request.
265 324
266 def __init__(self, src, dest, abssrc, absdest): 325 Args:
326 git_worktree: Absolute path to the git project checkout.
327 src: Relative path under |git_worktree| of file to read.
328 topdir: Absolute path to the top of the repo client checkout.
329 dest: Relative path under |topdir| of file to write.
330 """
331 self.git_worktree = git_worktree
332 self.topdir = topdir
267 self.src = src 333 self.src = src
268 self.dest = dest 334 self.dest = dest
269 self.abs_src = abssrc
270 self.abs_dest = absdest
271 335
272 def _Copy(self): 336 def _Copy(self):
273 src = self.abs_src 337 src = _SafeExpandPath(self.git_worktree, self.src)
274 dest = self.abs_dest 338 dest = _SafeExpandPath(self.topdir, self.dest)
339
340 if platform_utils.isdir(src):
341 raise ManifestInvalidPathError(
342 '%s: copying from directory not supported' % (self.src,))
343 if platform_utils.isdir(dest):
344 raise ManifestInvalidPathError(
345 '%s: copying to directory not allowed' % (self.dest,))
346
275 # copy file if it does not exist or is out of date 347 # copy file if it does not exist or is out of date
276 if not os.path.exists(dest) or not filecmp.cmp(src, dest): 348 if not os.path.exists(dest) or not filecmp.cmp(src, dest):
277 try: 349 try:
@@ -292,13 +364,21 @@ class _CopyFile(object):
292 364
293 365
294class _LinkFile(object): 366class _LinkFile(object):
367 """Container for <linkfile> manifest element."""
295 368
296 def __init__(self, git_worktree, src, dest, relsrc, absdest): 369 def __init__(self, git_worktree, src, topdir, dest):
370 """Register a <linkfile> request.
371
372 Args:
373 git_worktree: Absolute path to the git project checkout.
374 src: Target of symlink relative to path under |git_worktree|.
375 topdir: Absolute path to the top of the repo client checkout.
376 dest: Relative path under |topdir| of symlink to create.
377 """
297 self.git_worktree = git_worktree 378 self.git_worktree = git_worktree
379 self.topdir = topdir
298 self.src = src 380 self.src = src
299 self.dest = dest 381 self.dest = dest
300 self.src_rel_to_dest = relsrc
301 self.abs_dest = absdest
302 382
303 def __linkIt(self, relSrc, absDest): 383 def __linkIt(self, relSrc, absDest):
304 # link file if it does not exist or is out of date 384 # link file if it does not exist or is out of date
@@ -316,35 +396,42 @@ class _LinkFile(object):
316 _error('Cannot link file %s to %s', relSrc, absDest) 396 _error('Cannot link file %s to %s', relSrc, absDest)
317 397
318 def _Link(self): 398 def _Link(self):
319 """Link the self.rel_src_to_dest and self.abs_dest. Handles wild cards 399 """Link the self.src & self.dest paths.
320 on the src linking all of the files in the source in to the destination 400
321 directory. 401 Handles wild cards on the src linking all of the files in the source in to
402 the destination directory.
322 """ 403 """
323 # We use the absSrc to handle the situation where the current directory 404 # Some people use src="." to create stable links to projects. Lets allow
324 # is not the root of the repo 405 # that but reject all other uses of "." to keep things simple.
325 absSrc = os.path.join(self.git_worktree, self.src) 406 if self.src == '.':
326 if os.path.exists(absSrc): 407 src = self.git_worktree
327 # Entity exists so just a simple one to one link operation 408 else:
328 self.__linkIt(self.src_rel_to_dest, self.abs_dest) 409 src = _SafeExpandPath(self.git_worktree, self.src)
410
411 if not glob.has_magic(src):
412 # Entity does not contain a wild card so just a simple one to one link operation.
413 dest = _SafeExpandPath(self.topdir, self.dest, skipfinal=True)
414 # dest & src are absolute paths at this point. Make sure the target of
415 # the symlink is relative in the context of the repo client checkout.
416 relpath = os.path.relpath(src, os.path.dirname(dest))
417 self.__linkIt(relpath, dest)
329 else: 418 else:
330 # Entity doesn't exist assume there is a wild card 419 dest = _SafeExpandPath(self.topdir, self.dest)
331 absDestDir = self.abs_dest 420 # Entity contains a wild card.
332 if os.path.exists(absDestDir) and not platform_utils.isdir(absDestDir): 421 if os.path.exists(dest) and not platform_utils.isdir(dest):
333 _error('Link error: src with wildcard, %s must be a directory', 422 _error('Link error: src with wildcard, %s must be a directory', dest)
334 absDestDir)
335 else: 423 else:
336 absSrcFiles = glob.glob(absSrc) 424 for absSrcFile in glob.glob(src):
337 for absSrcFile in absSrcFiles:
338 # Create a releative path from source dir to destination dir 425 # Create a releative path from source dir to destination dir
339 absSrcDir = os.path.dirname(absSrcFile) 426 absSrcDir = os.path.dirname(absSrcFile)
340 relSrcDir = os.path.relpath(absSrcDir, absDestDir) 427 relSrcDir = os.path.relpath(absSrcDir, dest)
341 428
342 # Get the source file name 429 # Get the source file name
343 srcFile = os.path.basename(absSrcFile) 430 srcFile = os.path.basename(absSrcFile)
344 431
345 # Now form the final full paths to srcFile. They will be 432 # Now form the final full paths to srcFile. They will be
346 # absolute for the desintaiton and relative for the srouce. 433 # absolute for the desintaiton and relative for the srouce.
347 absDest = os.path.join(absDestDir, srcFile) 434 absDest = os.path.join(dest, srcFile)
348 relSrc = os.path.join(relSrcDir, srcFile) 435 relSrc = os.path.join(relSrcDir, srcFile)
349 self.__linkIt(relSrc, absDest) 436 self.__linkIt(relSrc, absDest)
350 437
@@ -368,405 +455,6 @@ class RemoteSpec(object):
368 self.fetchUrl = fetchUrl 455 self.fetchUrl = fetchUrl
369 456
370 457
371class RepoHook(object):
372
373 """A RepoHook contains information about a script to run as a hook.
374
375 Hooks are used to run a python script before running an upload (for instance,
376 to run presubmit checks). Eventually, we may have hooks for other actions.
377
378 This shouldn't be confused with files in the 'repo/hooks' directory. Those
379 files are copied into each '.git/hooks' folder for each project. Repo-level
380 hooks are associated instead with repo actions.
381
382 Hooks are always python. When a hook is run, we will load the hook into the
383 interpreter and execute its main() function.
384 """
385
386 def __init__(self,
387 hook_type,
388 hooks_project,
389 topdir,
390 manifest_url,
391 abort_if_user_denies=False):
392 """RepoHook constructor.
393
394 Params:
395 hook_type: A string representing the type of hook. This is also used
396 to figure out the name of the file containing the hook. For
397 example: 'pre-upload'.
398 hooks_project: The project containing the repo hooks. If you have a
399 manifest, this is manifest.repo_hooks_project. OK if this is None,
400 which will make the hook a no-op.
401 topdir: Repo's top directory (the one containing the .repo directory).
402 Scripts will run with CWD as this directory. If you have a manifest,
403 this is manifest.topdir
404 manifest_url: The URL to the manifest git repo.
405 abort_if_user_denies: If True, we'll throw a HookError() if the user
406 doesn't allow us to run the hook.
407 """
408 self._hook_type = hook_type
409 self._hooks_project = hooks_project
410 self._manifest_url = manifest_url
411 self._topdir = topdir
412 self._abort_if_user_denies = abort_if_user_denies
413
414 # Store the full path to the script for convenience.
415 if self._hooks_project:
416 self._script_fullpath = os.path.join(self._hooks_project.worktree,
417 self._hook_type + '.py')
418 else:
419 self._script_fullpath = None
420
421 def _GetHash(self):
422 """Return a hash of the contents of the hooks directory.
423
424 We'll just use git to do this. This hash has the property that if anything
425 changes in the directory we will return a different has.
426
427 SECURITY CONSIDERATION:
428 This hash only represents the contents of files in the hook directory, not
429 any other files imported or called by hooks. Changes to imported files
430 can change the script behavior without affecting the hash.
431
432 Returns:
433 A string representing the hash. This will always be ASCII so that it can
434 be printed to the user easily.
435 """
436 assert self._hooks_project, "Must have hooks to calculate their hash."
437
438 # We will use the work_git object rather than just calling GetRevisionId().
439 # That gives us a hash of the latest checked in version of the files that
440 # the user will actually be executing. Specifically, GetRevisionId()
441 # doesn't appear to change even if a user checks out a different version
442 # of the hooks repo (via git checkout) nor if a user commits their own revs.
443 #
444 # NOTE: Local (non-committed) changes will not be factored into this hash.
445 # I think this is OK, since we're really only worried about warning the user
446 # about upstream changes.
447 return self._hooks_project.work_git.rev_parse('HEAD')
448
449 def _GetMustVerb(self):
450 """Return 'must' if the hook is required; 'should' if not."""
451 if self._abort_if_user_denies:
452 return 'must'
453 else:
454 return 'should'
455
456 def _CheckForHookApproval(self):
457 """Check to see whether this hook has been approved.
458
459 We'll accept approval of manifest URLs if they're using secure transports.
460 This way the user can say they trust the manifest hoster. For insecure
461 hosts, we fall back to checking the hash of the hooks repo.
462
463 Note that we ask permission for each individual hook even though we use
464 the hash of all hooks when detecting changes. We'd like the user to be
465 able to approve / deny each hook individually. We only use the hash of all
466 hooks because there is no other easy way to detect changes to local imports.
467
468 Returns:
469 True if this hook is approved to run; False otherwise.
470
471 Raises:
472 HookError: Raised if the user doesn't approve and abort_if_user_denies
473 was passed to the consturctor.
474 """
475 if self._ManifestUrlHasSecureScheme():
476 return self._CheckForHookApprovalManifest()
477 else:
478 return self._CheckForHookApprovalHash()
479
480 def _CheckForHookApprovalHelper(self, subkey, new_val, main_prompt,
481 changed_prompt):
482 """Check for approval for a particular attribute and hook.
483
484 Args:
485 subkey: The git config key under [repo.hooks.<hook_type>] to store the
486 last approved string.
487 new_val: The new value to compare against the last approved one.
488 main_prompt: Message to display to the user to ask for approval.
489 changed_prompt: Message explaining why we're re-asking for approval.
490
491 Returns:
492 True if this hook is approved to run; False otherwise.
493
494 Raises:
495 HookError: Raised if the user doesn't approve and abort_if_user_denies
496 was passed to the consturctor.
497 """
498 hooks_config = self._hooks_project.config
499 git_approval_key = 'repo.hooks.%s.%s' % (self._hook_type, subkey)
500
501 # Get the last value that the user approved for this hook; may be None.
502 old_val = hooks_config.GetString(git_approval_key)
503
504 if old_val is not None:
505 # User previously approved hook and asked not to be prompted again.
506 if new_val == old_val:
507 # Approval matched. We're done.
508 return True
509 else:
510 # Give the user a reason why we're prompting, since they last told
511 # us to "never ask again".
512 prompt = 'WARNING: %s\n\n' % (changed_prompt,)
513 else:
514 prompt = ''
515
516 # Prompt the user if we're not on a tty; on a tty we'll assume "no".
517 if sys.stdout.isatty():
518 prompt += main_prompt + ' (yes/always/NO)? '
519 response = input(prompt).lower()
520 print()
521
522 # User is doing a one-time approval.
523 if response in ('y', 'yes'):
524 return True
525 elif response == 'always':
526 hooks_config.SetString(git_approval_key, new_val)
527 return True
528
529 # For anything else, we'll assume no approval.
530 if self._abort_if_user_denies:
531 raise HookError('You must allow the %s hook or use --no-verify.' %
532 self._hook_type)
533
534 return False
535
536 def _ManifestUrlHasSecureScheme(self):
537 """Check if the URI for the manifest is a secure transport."""
538 secure_schemes = ('file', 'https', 'ssh', 'persistent-https', 'sso', 'rpc')
539 parse_results = urllib.parse.urlparse(self._manifest_url)
540 return parse_results.scheme in secure_schemes
541
542 def _CheckForHookApprovalManifest(self):
543 """Check whether the user has approved this manifest host.
544
545 Returns:
546 True if this hook is approved to run; False otherwise.
547 """
548 return self._CheckForHookApprovalHelper(
549 'approvedmanifest',
550 self._manifest_url,
551 'Run hook scripts from %s' % (self._manifest_url,),
552 'Manifest URL has changed since %s was allowed.' % (self._hook_type,))
553
554 def _CheckForHookApprovalHash(self):
555 """Check whether the user has approved the hooks repo.
556
557 Returns:
558 True if this hook is approved to run; False otherwise.
559 """
560 prompt = ('Repo %s run the script:\n'
561 ' %s\n'
562 '\n'
563 'Do you want to allow this script to run')
564 return self._CheckForHookApprovalHelper(
565 'approvedhash',
566 self._GetHash(),
567 prompt % (self._GetMustVerb(), self._script_fullpath),
568 'Scripts have changed since %s was allowed.' % (self._hook_type,))
569
570 @staticmethod
571 def _ExtractInterpFromShebang(data):
572 """Extract the interpreter used in the shebang.
573
574 Try to locate the interpreter the script is using (ignoring `env`).
575
576 Args:
577 data: The file content of the script.
578
579 Returns:
580 The basename of the main script interpreter, or None if a shebang is not
581 used or could not be parsed out.
582 """
583 firstline = data.splitlines()[:1]
584 if not firstline:
585 return None
586
587 # The format here can be tricky.
588 shebang = firstline[0].strip()
589 m = re.match(r'^#!\s*([^\s]+)(?:\s+([^\s]+))?', shebang)
590 if not m:
591 return None
592
593 # If the using `env`, find the target program.
594 interp = m.group(1)
595 if os.path.basename(interp) == 'env':
596 interp = m.group(2)
597
598 return interp
599
600 def _ExecuteHookViaReexec(self, interp, context, **kwargs):
601 """Execute the hook script through |interp|.
602
603 Note: Support for this feature should be dropped ~Jun 2021.
604
605 Args:
606 interp: The Python program to run.
607 context: Basic Python context to execute the hook inside.
608 kwargs: Arbitrary arguments to pass to the hook script.
609
610 Raises:
611 HookError: When the hooks failed for any reason.
612 """
613 # This logic needs to be kept in sync with _ExecuteHookViaImport below.
614 script = """
615import json, os, sys
616path = '''%(path)s'''
617kwargs = json.loads('''%(kwargs)s''')
618context = json.loads('''%(context)s''')
619sys.path.insert(0, os.path.dirname(path))
620data = open(path).read()
621exec(compile(data, path, 'exec'), context)
622context['main'](**kwargs)
623""" % {
624 'path': self._script_fullpath,
625 'kwargs': json.dumps(kwargs),
626 'context': json.dumps(context),
627 }
628
629 # We pass the script via stdin to avoid OS argv limits. It also makes
630 # unhandled exception tracebacks less verbose/confusing for users.
631 cmd = [interp, '-c', 'import sys; exec(sys.stdin.read())']
632 proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
633 proc.communicate(input=script.encode('utf-8'))
634 if proc.returncode:
635 raise HookError('Failed to run %s hook.' % (self._hook_type,))
636
637 def _ExecuteHookViaImport(self, data, context, **kwargs):
638 """Execute the hook code in |data| directly.
639
640 Args:
641 data: The code of the hook to execute.
642 context: Basic Python context to execute the hook inside.
643 kwargs: Arbitrary arguments to pass to the hook script.
644
645 Raises:
646 HookError: When the hooks failed for any reason.
647 """
648 # Exec, storing global context in the context dict. We catch exceptions
649 # and convert to a HookError w/ just the failing traceback.
650 try:
651 exec(compile(data, self._script_fullpath, 'exec'), context)
652 except Exception:
653 raise HookError('%s\nFailed to import %s hook; see traceback above.' %
654 (traceback.format_exc(), self._hook_type))
655
656 # Running the script should have defined a main() function.
657 if 'main' not in context:
658 raise HookError('Missing main() in: "%s"' % self._script_fullpath)
659
660 # Call the main function in the hook. If the hook should cause the
661 # build to fail, it will raise an Exception. We'll catch that convert
662 # to a HookError w/ just the failing traceback.
663 try:
664 context['main'](**kwargs)
665 except Exception:
666 raise HookError('%s\nFailed to run main() for %s hook; see traceback '
667 'above.' % (traceback.format_exc(), self._hook_type))
668
669 def _ExecuteHook(self, **kwargs):
670 """Actually execute the given hook.
671
672 This will run the hook's 'main' function in our python interpreter.
673
674 Args:
675 kwargs: Keyword arguments to pass to the hook. These are often specific
676 to the hook type. For instance, pre-upload hooks will contain
677 a project_list.
678 """
679 # Keep sys.path and CWD stashed away so that we can always restore them
680 # upon function exit.
681 orig_path = os.getcwd()
682 orig_syspath = sys.path
683
684 try:
685 # Always run hooks with CWD as topdir.
686 os.chdir(self._topdir)
687
688 # Put the hook dir as the first item of sys.path so hooks can do
689 # relative imports. We want to replace the repo dir as [0] so
690 # hooks can't import repo files.
691 sys.path = [os.path.dirname(self._script_fullpath)] + sys.path[1:]
692
693 # Initial global context for the hook to run within.
694 context = {'__file__': self._script_fullpath}
695
696 # Add 'hook_should_take_kwargs' to the arguments to be passed to main.
697 # We don't actually want hooks to define their main with this argument--
698 # it's there to remind them that their hook should always take **kwargs.
699 # For instance, a pre-upload hook should be defined like:
700 # def main(project_list, **kwargs):
701 #
702 # This allows us to later expand the API without breaking old hooks.
703 kwargs = kwargs.copy()
704 kwargs['hook_should_take_kwargs'] = True
705
706 # See what version of python the hook has been written against.
707 data = open(self._script_fullpath).read()
708 interp = self._ExtractInterpFromShebang(data)
709 reexec = False
710 if interp:
711 prog = os.path.basename(interp)
712 if prog.startswith('python2') and sys.version_info.major != 2:
713 reexec = True
714 elif prog.startswith('python3') and sys.version_info.major == 2:
715 reexec = True
716
717 # Attempt to execute the hooks through the requested version of Python.
718 if reexec:
719 try:
720 self._ExecuteHookViaReexec(interp, context, **kwargs)
721 except OSError as e:
722 if e.errno == errno.ENOENT:
723 # We couldn't find the interpreter, so fallback to importing.
724 reexec = False
725 else:
726 raise
727
728 # Run the hook by importing directly.
729 if not reexec:
730 self._ExecuteHookViaImport(data, context, **kwargs)
731 finally:
732 # Restore sys.path and CWD.
733 sys.path = orig_syspath
734 os.chdir(orig_path)
735
736 def Run(self, user_allows_all_hooks, **kwargs):
737 """Run the hook.
738
739 If the hook doesn't exist (because there is no hooks project or because
740 this particular hook is not enabled), this is a no-op.
741
742 Args:
743 user_allows_all_hooks: If True, we will never prompt about running the
744 hook--we'll just assume it's OK to run it.
745 kwargs: Keyword arguments to pass to the hook. These are often specific
746 to the hook type. For instance, pre-upload hooks will contain
747 a project_list.
748
749 Raises:
750 HookError: If there was a problem finding the hook or the user declined
751 to run a required hook (from _CheckForHookApproval).
752 """
753 # No-op if there is no hooks project or if hook is disabled.
754 if ((not self._hooks_project) or (self._hook_type not in
755 self._hooks_project.enabled_repo_hooks)):
756 return
757
758 # Bail with a nice error if we can't find the hook.
759 if not os.path.isfile(self._script_fullpath):
760 raise HookError('Couldn\'t find repo hook: "%s"' % self._script_fullpath)
761
762 # Make sure the user is OK with running the hook.
763 if (not user_allows_all_hooks) and (not self._CheckForHookApproval()):
764 return
765
766 # Run the hook with the same version of python we're using.
767 self._ExecuteHook(**kwargs)
768
769
770class Project(object): 458class Project(object):
771 # These objects can be shared between several working trees. 459 # These objects can be shared between several working trees.
772 shareable_files = ['description', 'info'] 460 shareable_files = ['description', 'info']
@@ -793,9 +481,11 @@ class Project(object):
793 clone_depth=None, 481 clone_depth=None,
794 upstream=None, 482 upstream=None,
795 parent=None, 483 parent=None,
484 use_git_worktrees=False,
796 is_derived=False, 485 is_derived=False,
797 dest_branch=None, 486 dest_branch=None,
798 optimized_fetch=False, 487 optimized_fetch=False,
488 retry_fetches=0,
799 old_revision=None): 489 old_revision=None):
800 """Init a Project object. 490 """Init a Project object.
801 491
@@ -816,31 +506,21 @@ class Project(object):
816 sync_tags: The `sync-tags` attribute of manifest.xml's project element. 506 sync_tags: The `sync-tags` attribute of manifest.xml's project element.
817 upstream: The `upstream` attribute of manifest.xml's project element. 507 upstream: The `upstream` attribute of manifest.xml's project element.
818 parent: The parent Project object. 508 parent: The parent Project object.
509 use_git_worktrees: Whether to use `git worktree` for this project.
819 is_derived: False if the project was explicitly defined in the manifest; 510 is_derived: False if the project was explicitly defined in the manifest;
820 True if the project is a discovered submodule. 511 True if the project is a discovered submodule.
821 dest_branch: The branch to which to push changes for review by default. 512 dest_branch: The branch to which to push changes for review by default.
822 optimized_fetch: If True, when a project is set to a sha1 revision, only 513 optimized_fetch: If True, when a project is set to a sha1 revision, only
823 fetch from the remote if the sha1 is not present locally. 514 fetch from the remote if the sha1 is not present locally.
515 retry_fetches: Retry remote fetches n times upon receiving transient error
516 with exponential backoff and jitter.
824 old_revision: saved git commit id for open GITC projects. 517 old_revision: saved git commit id for open GITC projects.
825 """ 518 """
826 self.manifest = manifest 519 self.client = self.manifest = manifest
827 self.name = name 520 self.name = name
828 self.remote = remote 521 self.remote = remote
829 self.gitdir = gitdir.replace('\\', '/') 522 self.UpdatePaths(relpath, worktree, gitdir, objdir)
830 self.objdir = objdir.replace('\\', '/') 523 self.SetRevision(revisionExpr, revisionId=revisionId)
831 if worktree:
832 self.worktree = os.path.normpath(worktree).replace('\\', '/')
833 else:
834 self.worktree = None
835 self.relpath = relpath
836 self.revisionExpr = revisionExpr
837
838 if revisionId is None \
839 and revisionExpr \
840 and IsId(revisionExpr):
841 self.revisionId = revisionExpr
842 else:
843 self.revisionId = revisionId
844 524
845 self.rebase = rebase 525 self.rebase = rebase
846 self.groups = groups 526 self.groups = groups
@@ -850,24 +530,19 @@ class Project(object):
850 self.clone_depth = clone_depth 530 self.clone_depth = clone_depth
851 self.upstream = upstream 531 self.upstream = upstream
852 self.parent = parent 532 self.parent = parent
533 # NB: Do not use this setting in __init__ to change behavior so that the
534 # manifest.git checkout can inspect & change it after instantiating. See
535 # the XmlManifest init code for more info.
536 self.use_git_worktrees = use_git_worktrees
853 self.is_derived = is_derived 537 self.is_derived = is_derived
854 self.optimized_fetch = optimized_fetch 538 self.optimized_fetch = optimized_fetch
539 self.retry_fetches = max(0, retry_fetches)
855 self.subprojects = [] 540 self.subprojects = []
856 541
857 self.snapshots = {} 542 self.snapshots = {}
858 self.copyfiles = [] 543 self.copyfiles = []
859 self.linkfiles = [] 544 self.linkfiles = []
860 self.annotations = [] 545 self.annotations = []
861 self.config = GitConfig.ForRepository(gitdir=self.gitdir,
862 defaults=self.manifest.globalConfig)
863
864 if self.worktree:
865 self.work_git = self._GitGetByExec(self, bare=False, gitdir=gitdir)
866 else:
867 self.work_git = None
868 self.bare_git = self._GitGetByExec(self, bare=True, gitdir=gitdir)
869 self.bare_ref = GitRefs(gitdir)
870 self.bare_objdir = self._GitGetByExec(self, bare=True, gitdir=objdir)
871 self.dest_branch = dest_branch 546 self.dest_branch = dest_branch
872 self.old_revision = old_revision 547 self.old_revision = old_revision
873 548
@@ -875,6 +550,35 @@ class Project(object):
875 # project containing repo hooks. 550 # project containing repo hooks.
876 self.enabled_repo_hooks = [] 551 self.enabled_repo_hooks = []
877 552
553 def SetRevision(self, revisionExpr, revisionId=None):
554 """Set revisionId based on revision expression and id"""
555 self.revisionExpr = revisionExpr
556 if revisionId is None and revisionExpr and IsId(revisionExpr):
557 self.revisionId = self.revisionExpr
558 else:
559 self.revisionId = revisionId
560
561 def UpdatePaths(self, relpath, worktree, gitdir, objdir):
562 """Update paths used by this project"""
563 self.gitdir = gitdir.replace('\\', '/')
564 self.objdir = objdir.replace('\\', '/')
565 if worktree:
566 self.worktree = os.path.normpath(worktree).replace('\\', '/')
567 else:
568 self.worktree = None
569 self.relpath = relpath
570
571 self.config = GitConfig.ForRepository(gitdir=self.gitdir,
572 defaults=self.manifest.globalConfig)
573
574 if self.worktree:
575 self.work_git = self._GitGetByExec(self, bare=False, gitdir=self.gitdir)
576 else:
577 self.work_git = None
578 self.bare_git = self._GitGetByExec(self, bare=True, gitdir=self.gitdir)
579 self.bare_ref = GitRefs(self.gitdir)
580 self.bare_objdir = self._GitGetByExec(self, bare=True, gitdir=self.objdir)
581
878 @property 582 @property
879 def Derived(self): 583 def Derived(self):
880 return self.is_derived 584 return self.is_derived
@@ -902,11 +606,9 @@ class Project(object):
902 return None 606 return None
903 607
904 def IsRebaseInProgress(self): 608 def IsRebaseInProgress(self):
905 w = self.worktree 609 return (os.path.exists(self.work_git.GetDotgitPath('rebase-apply')) or
906 g = os.path.join(w, '.git') 610 os.path.exists(self.work_git.GetDotgitPath('rebase-merge')) or
907 return os.path.exists(os.path.join(g, 'rebase-apply')) \ 611 os.path.exists(os.path.join(self.worktree, '.dotest')))
908 or os.path.exists(os.path.join(g, 'rebase-merge')) \
909 or os.path.exists(os.path.join(w, '.dotest'))
910 612
911 def IsDirty(self, consider_untracked=True): 613 def IsDirty(self, consider_untracked=True):
912 """Is the working directory modified in some way? 614 """Is the working directory modified in some way?
@@ -1152,10 +854,12 @@ class Project(object):
1152 854
1153 return 'DIRTY' 855 return 'DIRTY'
1154 856
1155 def PrintWorkTreeDiff(self, absolute_paths=False): 857 def PrintWorkTreeDiff(self, absolute_paths=False, output_redir=None):
1156 """Prints the status of the repository to stdout. 858 """Prints the status of the repository to stdout.
1157 """ 859 """
1158 out = DiffColoring(self.config) 860 out = DiffColoring(self.config)
861 if output_redir:
862 out.redirect(output_redir)
1159 cmd = ['diff'] 863 cmd = ['diff']
1160 if out.is_on: 864 if out.is_on:
1161 cmd.append('--color') 865 cmd.append('--color')
@@ -1169,6 +873,7 @@ class Project(object):
1169 cmd, 873 cmd,
1170 capture_stdout=True, 874 capture_stdout=True,
1171 capture_stderr=True) 875 capture_stderr=True)
876 p.Wait()
1172 except GitError as e: 877 except GitError as e:
1173 out.nl() 878 out.nl()
1174 out.project('project %s/' % self.relpath) 879 out.project('project %s/' % self.relpath)
@@ -1176,21 +881,14 @@ class Project(object):
1176 out.fail('%s', str(e)) 881 out.fail('%s', str(e))
1177 out.nl() 882 out.nl()
1178 return False 883 return False
1179 has_diff = False 884 if p.stdout:
1180 for line in p.process.stdout: 885 out.nl()
1181 if not hasattr(line, 'encode'): 886 out.project('project %s/' % self.relpath)
1182 line = line.decode() 887 out.nl()
1183 if not has_diff: 888 out.write('%s', p.stdout)
1184 out.nl()
1185 out.project('project %s/' % self.relpath)
1186 out.nl()
1187 has_diff = True
1188 print(line[:-1])
1189 return p.Wait() == 0 889 return p.Wait() == 0
1190 890
1191
1192# Publish / Upload ## 891# Publish / Upload ##
1193
1194 def WasPublished(self, branch, all_refs=None): 892 def WasPublished(self, branch, all_refs=None):
1195 """Was the branch published (uploaded) for code review? 893 """Was the branch published (uploaded) for code review?
1196 If so, returns the SHA-1 hash of the last published 894 If so, returns the SHA-1 hash of the last published
@@ -1263,8 +961,10 @@ class Project(object):
1263 961
1264 def UploadForReview(self, branch=None, 962 def UploadForReview(self, branch=None,
1265 people=([], []), 963 people=([], []),
964 dryrun=False,
1266 auto_topic=False, 965 auto_topic=False,
1267 draft=False, 966 hashtags=(),
967 labels=(),
1268 private=False, 968 private=False,
1269 notify=None, 969 notify=None,
1270 wip=False, 970 wip=False,
@@ -1299,6 +999,8 @@ class Project(object):
1299 if url is None: 999 if url is None:
1300 raise UploadError('review not configured') 1000 raise UploadError('review not configured')
1301 cmd = ['push'] 1001 cmd = ['push']
1002 if dryrun:
1003 cmd.append('-n')
1302 1004
1303 if url.startswith('ssh://'): 1005 if url.startswith('ssh://'):
1304 cmd.append('--receive-pack=gerrit receive-pack') 1006 cmd.append('--receive-pack=gerrit receive-pack')
@@ -1312,15 +1014,12 @@ class Project(object):
1312 if dest_branch.startswith(R_HEADS): 1014 if dest_branch.startswith(R_HEADS):
1313 dest_branch = dest_branch[len(R_HEADS):] 1015 dest_branch = dest_branch[len(R_HEADS):]
1314 1016
1315 upload_type = 'for' 1017 ref_spec = '%s:refs/for/%s' % (R_HEADS + branch.name, dest_branch)
1316 if draft:
1317 upload_type = 'drafts'
1318
1319 ref_spec = '%s:refs/%s/%s' % (R_HEADS + branch.name, upload_type,
1320 dest_branch)
1321 opts = [] 1018 opts = []
1322 if auto_topic: 1019 if auto_topic:
1323 opts += ['topic=' + branch.name] 1020 opts += ['topic=' + branch.name]
1021 opts += ['t=%s' % p for p in hashtags]
1022 opts += ['l=%s' % p for p in labels]
1324 1023
1325 opts += ['r=%s' % p for p in people[0]] 1024 opts += ['r=%s' % p for p in people[0]]
1326 opts += ['cc=%s' % p for p in people[1]] 1025 opts += ['cc=%s' % p for p in people[1]]
@@ -1337,14 +1036,13 @@ class Project(object):
1337 if GitCommand(self, cmd, bare=True).Wait() != 0: 1036 if GitCommand(self, cmd, bare=True).Wait() != 0:
1338 raise UploadError('Upload failed') 1037 raise UploadError('Upload failed')
1339 1038
1340 msg = "posted to %s for %s" % (branch.remote.review, dest_branch) 1039 if not dryrun:
1341 self.bare_git.UpdateRef(R_PUB + branch.name, 1040 msg = "posted to %s for %s" % (branch.remote.review, dest_branch)
1342 R_HEADS + branch.name, 1041 self.bare_git.UpdateRef(R_PUB + branch.name,
1343 message=msg) 1042 R_HEADS + branch.name,
1344 1043 message=msg)
1345 1044
1346# Sync ## 1045# Sync ##
1347
1348 def _ExtractArchive(self, tarpath, path=None): 1046 def _ExtractArchive(self, tarpath, path=None):
1349 """Extract the given tar on its current location 1047 """Extract the given tar on its current location
1350 1048
@@ -1362,16 +1060,21 @@ class Project(object):
1362 1060
1363 def Sync_NetworkHalf(self, 1061 def Sync_NetworkHalf(self,
1364 quiet=False, 1062 quiet=False,
1063 verbose=False,
1064 output_redir=None,
1365 is_new=None, 1065 is_new=None,
1366 current_branch_only=False, 1066 current_branch_only=None,
1367 force_sync=False, 1067 force_sync=False,
1368 clone_bundle=True, 1068 clone_bundle=True,
1369 no_tags=False, 1069 tags=None,
1370 archive=False, 1070 archive=False,
1371 optimized_fetch=False, 1071 optimized_fetch=False,
1072 retry_fetches=0,
1372 prune=False, 1073 prune=False,
1373 submodules=False, 1074 submodules=False,
1374 clone_filter=None): 1075 ssh_proxy=None,
1076 clone_filter=None,
1077 partial_clone_exclude=set()):
1375 """Perform only the network IO portion of the sync process. 1078 """Perform only the network IO portion of the sync process.
1376 Local working directory/branch state is not affected. 1079 Local working directory/branch state is not affected.
1377 """ 1080 """
@@ -1402,12 +1105,22 @@ class Project(object):
1402 _warn("Cannot remove archive %s: %s", tarpath, str(e)) 1105 _warn("Cannot remove archive %s: %s", tarpath, str(e))
1403 self._CopyAndLinkFiles() 1106 self._CopyAndLinkFiles()
1404 return True 1107 return True
1108
1109 # If the shared object dir already exists, don't try to rebootstrap with a
1110 # clone bundle download. We should have the majority of objects already.
1111 if clone_bundle and os.path.exists(self.objdir):
1112 clone_bundle = False
1113
1114 if self.name in partial_clone_exclude:
1115 clone_bundle = True
1116 clone_filter = None
1117
1405 if is_new is None: 1118 if is_new is None:
1406 is_new = not self.Exists 1119 is_new = not self.Exists
1407 if is_new: 1120 if is_new:
1408 self._InitGitDir(force_sync=force_sync) 1121 self._InitGitDir(force_sync=force_sync, quiet=quiet)
1409 else: 1122 else:
1410 self._UpdateHooks() 1123 self._UpdateHooks(quiet=quiet)
1411 self._InitRemote() 1124 self._InitRemote()
1412 1125
1413 if is_new: 1126 if is_new:
@@ -1421,12 +1134,12 @@ class Project(object):
1421 else: 1134 else:
1422 alt_dir = None 1135 alt_dir = None
1423 1136
1424 if clone_bundle \ 1137 if (clone_bundle
1425 and alt_dir is None \ 1138 and alt_dir is None
1426 and self._ApplyCloneBundle(initial=is_new, quiet=quiet): 1139 and self._ApplyCloneBundle(initial=is_new, quiet=quiet, verbose=verbose)):
1427 is_new = False 1140 is_new = False
1428 1141
1429 if not current_branch_only: 1142 if current_branch_only is None:
1430 if self.sync_c: 1143 if self.sync_c:
1431 current_branch_only = True 1144 current_branch_only = True
1432 elif not self.manifest._loaded: 1145 elif not self.manifest._loaded:
@@ -1435,25 +1148,27 @@ class Project(object):
1435 elif self.manifest.default.sync_c: 1148 elif self.manifest.default.sync_c:
1436 current_branch_only = True 1149 current_branch_only = True
1437 1150
1438 if not no_tags: 1151 if tags is None:
1439 if not self.sync_tags: 1152 tags = self.sync_tags
1440 no_tags = True
1441 1153
1442 if self.clone_depth: 1154 if self.clone_depth:
1443 depth = self.clone_depth 1155 depth = self.clone_depth
1444 else: 1156 else:
1445 depth = self.manifest.manifestProject.config.GetString('repo.depth') 1157 depth = self.manifest.manifestProject.config.GetString('repo.depth')
1446 1158
1447 need_to_fetch = not (optimized_fetch and 1159 # See if we can skip the network fetch entirely.
1448 (ID_RE.match(self.revisionExpr) and 1160 if not (optimized_fetch and
1449 self._CheckForImmutableRevision())) 1161 (ID_RE.match(self.revisionExpr) and
1450 if (need_to_fetch and 1162 self._CheckForImmutableRevision())):
1451 not self._RemoteFetch(initial=is_new, quiet=quiet, alt_dir=alt_dir, 1163 if not self._RemoteFetch(
1452 current_branch_only=current_branch_only, 1164 initial=is_new,
1453 no_tags=no_tags, prune=prune, depth=depth, 1165 quiet=quiet, verbose=verbose, output_redir=output_redir,
1454 submodules=submodules, force_sync=force_sync, 1166 alt_dir=alt_dir, current_branch_only=current_branch_only,
1455 clone_filter=clone_filter)): 1167 tags=tags, prune=prune, depth=depth,
1456 return False 1168 submodules=submodules, force_sync=force_sync,
1169 ssh_proxy=ssh_proxy,
1170 clone_filter=clone_filter, retry_fetches=retry_fetches):
1171 return False
1457 1172
1458 mp = self.manifest.manifestProject 1173 mp = self.manifest.manifestProject
1459 dissociate = mp.config.GetBoolean('repo.dissociate') 1174 dissociate = mp.config.GetBoolean('repo.dissociate')
@@ -1461,7 +1176,11 @@ class Project(object):
1461 alternates_file = os.path.join(self.gitdir, 'objects/info/alternates') 1176 alternates_file = os.path.join(self.gitdir, 'objects/info/alternates')
1462 if os.path.exists(alternates_file): 1177 if os.path.exists(alternates_file):
1463 cmd = ['repack', '-a', '-d'] 1178 cmd = ['repack', '-a', '-d']
1464 if GitCommand(self, cmd, bare=True).Wait() != 0: 1179 p = GitCommand(self, cmd, bare=True, capture_stdout=bool(output_redir),
1180 merge_output=bool(output_redir))
1181 if p.stdout and output_redir:
1182 output_redir.write(p.stdout)
1183 if p.Wait() != 0:
1465 return False 1184 return False
1466 platform_utils.remove(alternates_file) 1185 platform_utils.remove(alternates_file)
1467 1186
@@ -1469,17 +1188,15 @@ class Project(object):
1469 self._InitMRef() 1188 self._InitMRef()
1470 else: 1189 else:
1471 self._InitMirrorHead() 1190 self._InitMirrorHead()
1472 try: 1191 platform_utils.remove(os.path.join(self.gitdir, 'FETCH_HEAD'),
1473 platform_utils.remove(os.path.join(self.gitdir, 'FETCH_HEAD')) 1192 missing_ok=True)
1474 except OSError:
1475 pass
1476 return True 1193 return True
1477 1194
1478 def PostRepoUpgrade(self): 1195 def PostRepoUpgrade(self):
1479 self._InitHooks() 1196 self._InitHooks()
1480 1197
1481 def _CopyAndLinkFiles(self): 1198 def _CopyAndLinkFiles(self):
1482 if self.manifest.isGitcClient: 1199 if self.client.isGitcClient:
1483 return 1200 return
1484 for copyfile in self.copyfiles: 1201 for copyfile in self.copyfiles:
1485 copyfile._Copy() 1202 copyfile._Copy()
@@ -1518,6 +1235,12 @@ class Project(object):
1518 raise ManifestInvalidRevisionError('revision %s in %s not found' % 1235 raise ManifestInvalidRevisionError('revision %s in %s not found' %
1519 (self.revisionExpr, self.name)) 1236 (self.revisionExpr, self.name))
1520 1237
1238 def SetRevisionId(self, revisionId):
1239 if self.revisionExpr:
1240 self.upstream = self.revisionExpr
1241
1242 self.revisionId = revisionId
1243
1521 def Sync_LocalHalf(self, syncbuf, force_sync=False, submodules=False): 1244 def Sync_LocalHalf(self, syncbuf, force_sync=False, submodules=False):
1522 """Perform only the local IO portion of the sync process. 1245 """Perform only the local IO portion of the sync process.
1523 Network access is not required. 1246 Network access is not required.
@@ -1534,6 +1257,18 @@ class Project(object):
1534 self.CleanPublishedCache(all_refs) 1257 self.CleanPublishedCache(all_refs)
1535 revid = self.GetRevisionId(all_refs) 1258 revid = self.GetRevisionId(all_refs)
1536 1259
1260 # Special case the root of the repo client checkout. Make sure it doesn't
1261 # contain files being checked out to dirs we don't allow.
1262 if self.relpath == '.':
1263 PROTECTED_PATHS = {'.repo'}
1264 paths = set(self.work_git.ls_tree('-z', '--name-only', '--', revid).split('\0'))
1265 bad_paths = paths & PROTECTED_PATHS
1266 if bad_paths:
1267 syncbuf.fail(self,
1268 'Refusing to checkout project that writes to protected '
1269 'paths: %s' % (', '.join(bad_paths),))
1270 return
1271
1537 def _doff(): 1272 def _doff():
1538 self._FastForward(revid) 1273 self._FastForward(revid)
1539 self._CopyAndLinkFiles() 1274 self._CopyAndLinkFiles()
@@ -1712,21 +1447,28 @@ class Project(object):
1712 if submodules: 1447 if submodules:
1713 syncbuf.later1(self, _dosubmodules) 1448 syncbuf.later1(self, _dosubmodules)
1714 1449
1715 def AddCopyFile(self, src, dest, absdest): 1450 def AddCopyFile(self, src, dest, topdir):
1716 # dest should already be an absolute path, but src is project relative 1451 """Mark |src| for copying to |dest| (relative to |topdir|).
1717 # make src an absolute path 1452
1718 abssrc = os.path.join(self.worktree, src) 1453 No filesystem changes occur here. Actual copying happens later on.
1719 self.copyfiles.append(_CopyFile(src, dest, abssrc, absdest)) 1454
1455 Paths should have basic validation run on them before being queued.
1456 Further checking will be handled when the actual copy happens.
1457 """
1458 self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest))
1720 1459
1721 def AddLinkFile(self, src, dest, absdest): 1460 def AddLinkFile(self, src, dest, topdir):
1722 # dest should already be an absolute path, but src is project relative 1461 """Mark |dest| to create a symlink (relative to |topdir|) pointing to |src|.
1723 # make src relative path to dest 1462
1724 absdestdir = os.path.dirname(absdest) 1463 No filesystem changes occur here. Actual linking happens later on.
1725 relsrc = os.path.relpath(os.path.join(self.worktree, src), absdestdir) 1464
1726 self.linkfiles.append(_LinkFile(self.worktree, src, dest, relsrc, absdest)) 1465 Paths should have basic validation run on them before being queued.
1466 Further checking will be handled when the actual link happens.
1467 """
1468 self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
1727 1469
1728 def AddAnnotation(self, name, value, keep): 1470 def AddAnnotation(self, name, value, keep):
1729 self.annotations.append(_Annotation(name, value, keep)) 1471 self.annotations.append(Annotation(name, value, keep))
1730 1472
1731 def DownloadPatchSet(self, change_id, patch_id): 1473 def DownloadPatchSet(self, change_id, patch_id):
1732 """Download a single patch set of a single change to FETCH_HEAD. 1474 """Download a single patch set of a single change to FETCH_HEAD.
@@ -1744,9 +1486,123 @@ class Project(object):
1744 patch_id, 1486 patch_id,
1745 self.bare_git.rev_parse('FETCH_HEAD')) 1487 self.bare_git.rev_parse('FETCH_HEAD'))
1746 1488
1489 def DeleteWorktree(self, quiet=False, force=False):
1490 """Delete the source checkout and any other housekeeping tasks.
1747 1491
1748# Branch Management ## 1492 This currently leaves behind the internal .repo/ cache state. This helps
1493 when switching branches or manifest changes get reverted as we don't have
1494 to redownload all the git objects. But we should do some GC at some point.
1495
1496 Args:
1497 quiet: Whether to hide normal messages.
1498 force: Always delete tree even if dirty.
1499
1500 Returns:
1501 True if the worktree was completely cleaned out.
1502 """
1503 if self.IsDirty():
1504 if force:
1505 print('warning: %s: Removing dirty project: uncommitted changes lost.' %
1506 (self.relpath,), file=sys.stderr)
1507 else:
1508 print('error: %s: Cannot remove project: uncommitted changes are '
1509 'present.\n' % (self.relpath,), file=sys.stderr)
1510 return False
1511
1512 if not quiet:
1513 print('%s: Deleting obsolete checkout.' % (self.relpath,))
1514
1515 # Unlock and delink from the main worktree. We don't use git's worktree
1516 # remove because it will recursively delete projects -- we handle that
1517 # ourselves below. https://crbug.com/git/48
1518 if self.use_git_worktrees:
1519 needle = platform_utils.realpath(self.gitdir)
1520 # Find the git worktree commondir under .repo/worktrees/.
1521 output = self.bare_git.worktree('list', '--porcelain').splitlines()[0]
1522 assert output.startswith('worktree '), output
1523 commondir = output[9:]
1524 # Walk each of the git worktrees to see where they point.
1525 configs = os.path.join(commondir, 'worktrees')
1526 for name in os.listdir(configs):
1527 gitdir = os.path.join(configs, name, 'gitdir')
1528 with open(gitdir) as fp:
1529 relpath = fp.read().strip()
1530 # Resolve the checkout path and see if it matches this project.
1531 fullpath = platform_utils.realpath(os.path.join(configs, name, relpath))
1532 if fullpath == needle:
1533 platform_utils.rmtree(os.path.join(configs, name))
1534
1535 # Delete the .git directory first, so we're less likely to have a partially
1536 # working git repository around. There shouldn't be any git projects here,
1537 # so rmtree works.
1538
1539 # Try to remove plain files first in case of git worktrees. If this fails
1540 # for any reason, we'll fall back to rmtree, and that'll display errors if
1541 # it can't remove things either.
1542 try:
1543 platform_utils.remove(self.gitdir)
1544 except OSError:
1545 pass
1546 try:
1547 platform_utils.rmtree(self.gitdir)
1548 except OSError as e:
1549 if e.errno != errno.ENOENT:
1550 print('error: %s: %s' % (self.gitdir, e), file=sys.stderr)
1551 print('error: %s: Failed to delete obsolete checkout; remove manually, '
1552 'then run `repo sync -l`.' % (self.relpath,), file=sys.stderr)
1553 return False
1554
1555 # Delete everything under the worktree, except for directories that contain
1556 # another git project.
1557 dirs_to_remove = []
1558 failed = False
1559 for root, dirs, files in platform_utils.walk(self.worktree):
1560 for f in files:
1561 path = os.path.join(root, f)
1562 try:
1563 platform_utils.remove(path)
1564 except OSError as e:
1565 if e.errno != errno.ENOENT:
1566 print('error: %s: Failed to remove: %s' % (path, e), file=sys.stderr)
1567 failed = True
1568 dirs[:] = [d for d in dirs
1569 if not os.path.lexists(os.path.join(root, d, '.git'))]
1570 dirs_to_remove += [os.path.join(root, d) for d in dirs
1571 if os.path.join(root, d) not in dirs_to_remove]
1572 for d in reversed(dirs_to_remove):
1573 if platform_utils.islink(d):
1574 try:
1575 platform_utils.remove(d)
1576 except OSError as e:
1577 if e.errno != errno.ENOENT:
1578 print('error: %s: Failed to remove: %s' % (d, e), file=sys.stderr)
1579 failed = True
1580 elif not platform_utils.listdir(d):
1581 try:
1582 platform_utils.rmdir(d)
1583 except OSError as e:
1584 if e.errno != errno.ENOENT:
1585 print('error: %s: Failed to remove: %s' % (d, e), file=sys.stderr)
1586 failed = True
1587 if failed:
1588 print('error: %s: Failed to delete obsolete checkout.' % (self.relpath,),
1589 file=sys.stderr)
1590 print(' Remove manually, then run `repo sync -l`.', file=sys.stderr)
1591 return False
1749 1592
1593 # Try deleting parent dirs if they are empty.
1594 path = self.worktree
1595 while path != self.manifest.topdir:
1596 try:
1597 platform_utils.rmdir(path)
1598 except OSError as e:
1599 if e.errno != errno.ENOENT:
1600 break
1601 path = os.path.dirname(path)
1602
1603 return True
1604
1605# Branch Management ##
1750 def StartBranch(self, name, branch_merge='', revision=None): 1606 def StartBranch(self, name, branch_merge='', revision=None):
1751 """Create a new branch off the manifest's revision. 1607 """Create a new branch off the manifest's revision.
1752 """ 1608 """
@@ -1780,14 +1636,9 @@ class Project(object):
1780 except KeyError: 1636 except KeyError:
1781 head = None 1637 head = None
1782 if revid and head and revid == head: 1638 if revid and head and revid == head:
1783 ref = os.path.join(self.gitdir, R_HEADS + name) 1639 ref = R_HEADS + name
1784 try: 1640 self.work_git.update_ref(ref, revid)
1785 os.makedirs(os.path.dirname(ref)) 1641 self.work_git.symbolic_ref(HEAD, ref)
1786 except OSError:
1787 pass
1788 _lwrite(ref, '%s\n' % revid)
1789 _lwrite(os.path.join(self.worktree, '.git', HEAD),
1790 'ref: %s%s\n' % (R_HEADS, name))
1791 branch.Save() 1642 branch.Save()
1792 return True 1643 return True
1793 1644
@@ -1834,7 +1685,7 @@ class Project(object):
1834 # Same revision; just update HEAD to point to the new 1685 # Same revision; just update HEAD to point to the new
1835 # target branch, but otherwise take no other action. 1686 # target branch, but otherwise take no other action.
1836 # 1687 #
1837 _lwrite(os.path.join(self.worktree, '.git', HEAD), 1688 _lwrite(self.work_git.GetDotgitPath(subpath=HEAD),
1838 'ref: %s%s\n' % (R_HEADS, name)) 1689 'ref: %s%s\n' % (R_HEADS, name))
1839 return True 1690 return True
1840 1691
@@ -1868,8 +1719,7 @@ class Project(object):
1868 1719
1869 revid = self.GetRevisionId(all_refs) 1720 revid = self.GetRevisionId(all_refs)
1870 if head == revid: 1721 if head == revid:
1871 _lwrite(os.path.join(self.worktree, '.git', HEAD), 1722 _lwrite(self.work_git.GetDotgitPath(subpath=HEAD), '%s\n' % revid)
1872 '%s\n' % revid)
1873 else: 1723 else:
1874 self._Checkout(revid, quiet=True) 1724 self._Checkout(revid, quiet=True)
1875 1725
@@ -1890,6 +1740,11 @@ class Project(object):
1890 if cb is None or name != cb: 1740 if cb is None or name != cb:
1891 kill.append(name) 1741 kill.append(name)
1892 1742
1743 # Minor optimization: If there's nothing to prune, then don't try to read
1744 # any project state.
1745 if not kill and not cb:
1746 return []
1747
1893 rev = self.GetRevisionId(left) 1748 rev = self.GetRevisionId(left)
1894 if cb is not None \ 1749 if cb is not None \
1895 and not self._revlist(HEAD + '...' + rev) \ 1750 and not self._revlist(HEAD + '...' + rev) \
@@ -1935,9 +1790,7 @@ class Project(object):
1935 kept.append(ReviewableBranch(self, branch, base)) 1790 kept.append(ReviewableBranch(self, branch, base))
1936 return kept 1791 return kept
1937 1792
1938
1939# Submodule Management ## 1793# Submodule Management ##
1940
1941 def GetRegisteredSubprojects(self): 1794 def GetRegisteredSubprojects(self):
1942 result = [] 1795 result = []
1943 1796
@@ -2088,13 +1941,57 @@ class Project(object):
2088 result.extend(subproject.GetDerivedSubprojects()) 1941 result.extend(subproject.GetDerivedSubprojects())
2089 return result 1942 return result
2090 1943
2091
2092# Direct Git Commands ## 1944# Direct Git Commands ##
1945 def EnableRepositoryExtension(self, key, value='true', version=1):
1946 """Enable git repository extension |key| with |value|.
1947
1948 Args:
1949 key: The extension to enabled. Omit the "extensions." prefix.
1950 value: The value to use for the extension.
1951 version: The minimum git repository version needed.
1952 """
1953 # Make sure the git repo version is new enough already.
1954 found_version = self.config.GetInt('core.repositoryFormatVersion')
1955 if found_version is None:
1956 found_version = 0
1957 if found_version < version:
1958 self.config.SetString('core.repositoryFormatVersion', str(version))
1959
1960 # Enable the extension!
1961 self.config.SetString('extensions.%s' % (key,), value)
1962
1963 def ResolveRemoteHead(self, name=None):
1964 """Find out what the default branch (HEAD) points to.
1965
1966 Normally this points to refs/heads/master, but projects are moving to main.
1967 Support whatever the server uses rather than hardcoding "master" ourselves.
1968 """
1969 if name is None:
1970 name = self.remote.name
1971
1972 # The output will look like (NB: tabs are separators):
1973 # ref: refs/heads/master HEAD
1974 # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
1975 output = self.bare_git.ls_remote('-q', '--symref', '--exit-code', name, 'HEAD')
1976
1977 for line in output.splitlines():
1978 lhs, rhs = line.split('\t', 1)
1979 if rhs == 'HEAD' and lhs.startswith('ref:'):
1980 return lhs[4:].strip()
1981
1982 return None
1983
2093 def _CheckForImmutableRevision(self): 1984 def _CheckForImmutableRevision(self):
2094 try: 1985 try:
2095 # if revision (sha or tag) is not present then following function 1986 # if revision (sha or tag) is not present then following function
2096 # throws an error. 1987 # throws an error.
2097 self.bare_git.rev_parse('--verify', '%s^0' % self.revisionExpr) 1988 self.bare_git.rev_list('-1', '--missing=allow-any',
1989 '%s^0' % self.revisionExpr, '--')
1990 if self.upstream:
1991 rev = self.GetRemote(self.remote.name).ToLocal(self.upstream)
1992 self.bare_git.rev_list('-1', '--missing=allow-any',
1993 '%s^0' % rev, '--')
1994 self.bare_git.merge_base('--is-ancestor', self.revisionExpr, rev)
2098 return True 1995 return True
2099 except GitError: 1996 except GitError:
2100 # There is no such persistent revision. We have to fetch it. 1997 # There is no such persistent revision. We have to fetch it.
@@ -2117,14 +2014,19 @@ class Project(object):
2117 current_branch_only=False, 2014 current_branch_only=False,
2118 initial=False, 2015 initial=False,
2119 quiet=False, 2016 quiet=False,
2017 verbose=False,
2018 output_redir=None,
2120 alt_dir=None, 2019 alt_dir=None,
2121 no_tags=False, 2020 tags=True,
2122 prune=False, 2021 prune=False,
2123 depth=None, 2022 depth=None,
2124 submodules=False, 2023 submodules=False,
2024 ssh_proxy=None,
2125 force_sync=False, 2025 force_sync=False,
2126 clone_filter=None): 2026 clone_filter=None,
2127 2027 retry_fetches=2,
2028 retry_sleep_initial_sec=4.0,
2029 retry_exp_factor=2.0):
2128 is_sha1 = False 2030 is_sha1 = False
2129 tag_name = None 2031 tag_name = None
2130 # The depth should not be used when fetching to a mirror because 2032 # The depth should not be used when fetching to a mirror because
@@ -2147,7 +2049,7 @@ class Project(object):
2147 2049
2148 if is_sha1 or tag_name is not None: 2050 if is_sha1 or tag_name is not None:
2149 if self._CheckForImmutableRevision(): 2051 if self._CheckForImmutableRevision():
2150 if not quiet: 2052 if verbose:
2151 print('Skipped fetching project %s (already have persistent ref)' 2053 print('Skipped fetching project %s (already have persistent ref)'
2152 % self.name) 2054 % self.name)
2153 return True 2055 return True
@@ -2167,16 +2069,14 @@ class Project(object):
2167 if not name: 2069 if not name:
2168 name = self.remote.name 2070 name = self.remote.name
2169 2071
2170 ssh_proxy = False
2171 remote = self.GetRemote(name) 2072 remote = self.GetRemote(name)
2172 if remote.PreConnectFetch(): 2073 if not remote.PreConnectFetch(ssh_proxy):
2173 ssh_proxy = True 2074 ssh_proxy = None
2174 2075
2175 if initial: 2076 if initial:
2176 if alt_dir and 'objects' == os.path.basename(alt_dir): 2077 if alt_dir and 'objects' == os.path.basename(alt_dir):
2177 ref_dir = os.path.dirname(alt_dir) 2078 ref_dir = os.path.dirname(alt_dir)
2178 packed_refs = os.path.join(self.gitdir, 'packed-refs') 2079 packed_refs = os.path.join(self.gitdir, 'packed-refs')
2179 remote = self.GetRemote(name)
2180 2080
2181 all_refs = self.bare_ref.all 2081 all_refs = self.bare_ref.all
2182 ids = set(all_refs.values()) 2082 ids = set(all_refs.values())
@@ -2217,7 +2117,7 @@ class Project(object):
2217 if clone_filter: 2117 if clone_filter:
2218 git_require((2, 19, 0), fail=True, msg='partial clones') 2118 git_require((2, 19, 0), fail=True, msg='partial clones')
2219 cmd.append('--filter=%s' % clone_filter) 2119 cmd.append('--filter=%s' % clone_filter)
2220 self.config.SetString('extensions.partialclone', self.remote.name) 2120 self.EnableRepositoryExtension('partialclone', self.remote.name)
2221 2121
2222 if depth: 2122 if depth:
2223 cmd.append('--depth=%s' % depth) 2123 cmd.append('--depth=%s' % depth)
@@ -2229,8 +2129,10 @@ class Project(object):
2229 if os.path.exists(os.path.join(self.gitdir, 'shallow')): 2129 if os.path.exists(os.path.join(self.gitdir, 'shallow')):
2230 cmd.append('--depth=2147483647') 2130 cmd.append('--depth=2147483647')
2231 2131
2232 if quiet: 2132 if not verbose:
2233 cmd.append('--quiet') 2133 cmd.append('--quiet')
2134 if not quiet and sys.stdout.isatty():
2135 cmd.append('--progress')
2234 if not self.worktree: 2136 if not self.worktree:
2235 cmd.append('--update-head-ok') 2137 cmd.append('--update-head-ok')
2236 cmd.append(name) 2138 cmd.append(name)
@@ -2257,10 +2159,12 @@ class Project(object):
2257 else: 2159 else:
2258 branch = self.revisionExpr 2160 branch = self.revisionExpr
2259 if (not self.manifest.IsMirror and is_sha1 and depth 2161 if (not self.manifest.IsMirror and is_sha1 and depth
2260 and git_require((1, 8, 3))): 2162 and git_require((1, 8, 3))):
2261 # Shallow checkout of a specific commit, fetch from that commit and not 2163 # Shallow checkout of a specific commit, fetch from that commit and not
2262 # the heads only as the commit might be deeper in the history. 2164 # the heads only as the commit might be deeper in the history.
2263 spec.append(branch) 2165 spec.append(branch)
2166 if self.upstream:
2167 spec.append(self.upstream)
2264 else: 2168 else:
2265 if is_sha1: 2169 if is_sha1:
2266 branch = self.upstream 2170 branch = self.upstream
@@ -2276,7 +2180,7 @@ class Project(object):
2276 2180
2277 # If using depth then we should not get all the tags since they may 2181 # If using depth then we should not get all the tags since they may
2278 # be outside of the depth. 2182 # be outside of the depth.
2279 if no_tags or depth: 2183 if not tags or depth:
2280 cmd.append('--no-tags') 2184 cmd.append('--no-tags')
2281 else: 2185 else:
2282 cmd.append('--tags') 2186 cmd.append('--tags')
@@ -2284,22 +2188,42 @@ class Project(object):
2284 2188
2285 cmd.extend(spec) 2189 cmd.extend(spec)
2286 2190
2287 ok = False 2191 # At least one retry minimum due to git remote prune.
2288 for _i in range(2): 2192 retry_fetches = max(retry_fetches, 2)
2289 gitcmd = GitCommand(self, cmd, bare=True, ssh_proxy=ssh_proxy) 2193 retry_cur_sleep = retry_sleep_initial_sec
2194 ok = prune_tried = False
2195 for try_n in range(retry_fetches):
2196 gitcmd = GitCommand(self, cmd, bare=True, ssh_proxy=ssh_proxy,
2197 merge_output=True, capture_stdout=quiet or bool(output_redir))
2198 if gitcmd.stdout and not quiet and output_redir:
2199 output_redir.write(gitcmd.stdout)
2290 ret = gitcmd.Wait() 2200 ret = gitcmd.Wait()
2291 if ret == 0: 2201 if ret == 0:
2292 ok = True 2202 ok = True
2293 break 2203 break
2294 # If needed, run the 'git remote prune' the first time through the loop 2204
2295 elif (not _i and 2205 # Retry later due to HTTP 429 Too Many Requests.
2296 "error:" in gitcmd.stderr and 2206 elif (gitcmd.stdout and
2297 "git remote prune" in gitcmd.stderr): 2207 'error:' in gitcmd.stdout and
2208 'HTTP 429' in gitcmd.stdout):
2209 # Fallthru to sleep+retry logic at the bottom.
2210 pass
2211
2212 # Try to prune remote branches once in case there are conflicts.
2213 # For example, if the remote had refs/heads/upstream, but deleted that and
2214 # now has refs/heads/upstream/foo.
2215 elif (gitcmd.stdout and
2216 'error:' in gitcmd.stdout and
2217 'git remote prune' in gitcmd.stdout and
2218 not prune_tried):
2219 prune_tried = True
2298 prunecmd = GitCommand(self, ['remote', 'prune', name], bare=True, 2220 prunecmd = GitCommand(self, ['remote', 'prune', name], bare=True,
2299 ssh_proxy=ssh_proxy) 2221 ssh_proxy=ssh_proxy)
2300 ret = prunecmd.Wait() 2222 ret = prunecmd.Wait()
2301 if ret: 2223 if ret:
2302 break 2224 break
2225 print('retrying fetch after pruning remote branches', file=output_redir)
2226 # Continue right away so we don't sleep as we shouldn't need to.
2303 continue 2227 continue
2304 elif current_branch_only and is_sha1 and ret == 128: 2228 elif current_branch_only and is_sha1 and ret == 128:
2305 # Exit code 128 means "couldn't find the ref you asked for"; if we're 2229 # Exit code 128 means "couldn't find the ref you asked for"; if we're
@@ -2309,7 +2233,18 @@ class Project(object):
2309 elif ret < 0: 2233 elif ret < 0:
2310 # Git died with a signal, exit immediately 2234 # Git died with a signal, exit immediately
2311 break 2235 break
2312 time.sleep(random.randint(30, 45)) 2236
2237 # Figure out how long to sleep before the next attempt, if there is one.
2238 if not verbose and gitcmd.stdout:
2239 print('\n%s:\n%s' % (self.name, gitcmd.stdout), end='', file=output_redir)
2240 if try_n < retry_fetches - 1:
2241 print('%s: sleeping %s seconds before retrying' % (self.name, retry_cur_sleep),
2242 file=output_redir)
2243 time.sleep(retry_cur_sleep)
2244 retry_cur_sleep = min(retry_exp_factor * retry_cur_sleep,
2245 MAXIMUM_RETRY_SLEEP_SEC)
2246 retry_cur_sleep *= (1 - random.uniform(-RETRY_JITTER_PERCENT,
2247 RETRY_JITTER_PERCENT))
2313 2248
2314 if initial: 2249 if initial:
2315 if alt_dir: 2250 if alt_dir:
@@ -2324,21 +2259,17 @@ class Project(object):
2324 # got what we wanted, else trigger a second run of all 2259 # got what we wanted, else trigger a second run of all
2325 # refs. 2260 # refs.
2326 if not self._CheckForImmutableRevision(): 2261 if not self._CheckForImmutableRevision():
2327 if current_branch_only and depth: 2262 # Sync the current branch only with depth set to None.
2328 # Sync the current branch only with depth set to None 2263 # We always pass depth=None down to avoid infinite recursion.
2329 return self._RemoteFetch(name=name, 2264 return self._RemoteFetch(
2330 current_branch_only=current_branch_only, 2265 name=name, quiet=quiet, verbose=verbose, output_redir=output_redir,
2331 initial=False, quiet=quiet, alt_dir=alt_dir, 2266 current_branch_only=current_branch_only and depth,
2332 depth=None, clone_filter=clone_filter) 2267 initial=False, alt_dir=alt_dir,
2333 else: 2268 depth=None, ssh_proxy=ssh_proxy, clone_filter=clone_filter)
2334 # Avoid infinite recursion: sync all branches with depth set to None
2335 return self._RemoteFetch(name=name, current_branch_only=False,
2336 initial=False, quiet=quiet, alt_dir=alt_dir,
2337 depth=None, clone_filter=clone_filter)
2338 2269
2339 return ok 2270 return ok
2340 2271
2341 def _ApplyCloneBundle(self, initial=False, quiet=False): 2272 def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
2342 if initial and \ 2273 if initial and \
2343 (self.manifest.manifestProject.config.GetString('repo.depth') or 2274 (self.manifest.manifestProject.config.GetString('repo.depth') or
2344 self.clone_depth): 2275 self.clone_depth):
@@ -2362,13 +2293,16 @@ class Project(object):
2362 return False 2293 return False
2363 2294
2364 if not exist_dst: 2295 if not exist_dst:
2365 exist_dst = self._FetchBundle(bundle_url, bundle_tmp, bundle_dst, quiet) 2296 exist_dst = self._FetchBundle(bundle_url, bundle_tmp, bundle_dst, quiet,
2297 verbose)
2366 if not exist_dst: 2298 if not exist_dst:
2367 return False 2299 return False
2368 2300
2369 cmd = ['fetch'] 2301 cmd = ['fetch']
2370 if quiet: 2302 if not verbose:
2371 cmd.append('--quiet') 2303 cmd.append('--quiet')
2304 if not quiet and sys.stdout.isatty():
2305 cmd.append('--progress')
2372 if not self.worktree: 2306 if not self.worktree:
2373 cmd.append('--update-head-ok') 2307 cmd.append('--update-head-ok')
2374 cmd.append(bundle_dst) 2308 cmd.append(bundle_dst)
@@ -2377,19 +2311,16 @@ class Project(object):
2377 cmd.append('+refs/tags/*:refs/tags/*') 2311 cmd.append('+refs/tags/*:refs/tags/*')
2378 2312
2379 ok = GitCommand(self, cmd, bare=True).Wait() == 0 2313 ok = GitCommand(self, cmd, bare=True).Wait() == 0
2380 if os.path.exists(bundle_dst): 2314 platform_utils.remove(bundle_dst, missing_ok=True)
2381 platform_utils.remove(bundle_dst) 2315 platform_utils.remove(bundle_tmp, missing_ok=True)
2382 if os.path.exists(bundle_tmp):
2383 platform_utils.remove(bundle_tmp)
2384 return ok 2316 return ok
2385 2317
2386 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet): 2318 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2387 if os.path.exists(dstPath): 2319 platform_utils.remove(dstPath, missing_ok=True)
2388 platform_utils.remove(dstPath)
2389 2320
2390 cmd = ['curl', '--fail', '--output', tmpPath, '--netrc', '--location'] 2321 cmd = ['curl', '--fail', '--output', tmpPath, '--netrc', '--location']
2391 if quiet: 2322 if quiet:
2392 cmd += ['--silent'] 2323 cmd += ['--silent', '--show-error']
2393 if os.path.exists(tmpPath): 2324 if os.path.exists(tmpPath):
2394 size = os.stat(tmpPath).st_size 2325 size = os.stat(tmpPath).st_size
2395 if size >= 1024: 2326 if size >= 1024:
@@ -2411,22 +2342,30 @@ class Project(object):
2411 2342
2412 if IsTrace(): 2343 if IsTrace():
2413 Trace('%s', ' '.join(cmd)) 2344 Trace('%s', ' '.join(cmd))
2345 if verbose:
2346 print('%s: Downloading bundle: %s' % (self.name, srcUrl))
2347 stdout = None if verbose else subprocess.PIPE
2348 stderr = None if verbose else subprocess.STDOUT
2414 try: 2349 try:
2415 proc = subprocess.Popen(cmd) 2350 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
2416 except OSError: 2351 except OSError:
2417 return False 2352 return False
2418 2353
2419 curlret = proc.wait() 2354 (output, _) = proc.communicate()
2355 curlret = proc.returncode
2420 2356
2421 if curlret == 22: 2357 if curlret == 22:
2422 # From curl man page: 2358 # From curl man page:
2423 # 22: HTTP page not retrieved. The requested url was not found or 2359 # 22: HTTP page not retrieved. The requested url was not found or
2424 # returned another error with the HTTP error code being 400 or above. 2360 # returned another error with the HTTP error code being 400 or above.
2425 # This return code only appears if -f, --fail is used. 2361 # This return code only appears if -f, --fail is used.
2426 if not quiet: 2362 if verbose:
2427 print("Server does not provide clone.bundle; ignoring.", 2363 print('%s: Unable to retrieve clone.bundle; ignoring.' % self.name)
2428 file=sys.stderr) 2364 if output:
2365 print('Curl output:\n%s' % output)
2429 return False 2366 return False
2367 elif curlret and not verbose and output:
2368 print('%s' % output, file=sys.stderr)
2430 2369
2431 if os.path.exists(tmpPath): 2370 if os.path.exists(tmpPath):
2432 if curlret == 0 and self._IsValidBundle(tmpPath, quiet): 2371 if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
@@ -2460,8 +2399,12 @@ class Project(object):
2460 if self._allrefs: 2399 if self._allrefs:
2461 raise GitError('%s checkout %s ' % (self.name, rev)) 2400 raise GitError('%s checkout %s ' % (self.name, rev))
2462 2401
2463 def _CherryPick(self, rev): 2402 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2464 cmd = ['cherry-pick'] 2403 cmd = ['cherry-pick']
2404 if ffonly:
2405 cmd.append('--ff')
2406 if record_origin:
2407 cmd.append('-x')
2465 cmd.append(rev) 2408 cmd.append(rev)
2466 cmd.append('--') 2409 cmd.append('--')
2467 if GitCommand(self, cmd).Wait() != 0: 2410 if GitCommand(self, cmd).Wait() != 0:
@@ -2508,13 +2451,13 @@ class Project(object):
2508 raise GitError('%s rebase %s ' % (self.name, upstream)) 2451 raise GitError('%s rebase %s ' % (self.name, upstream))
2509 2452
2510 def _FastForward(self, head, ffonly=False): 2453 def _FastForward(self, head, ffonly=False):
2511 cmd = ['merge', head] 2454 cmd = ['merge', '--no-stat', head]
2512 if ffonly: 2455 if ffonly:
2513 cmd.append("--ff-only") 2456 cmd.append("--ff-only")
2514 if GitCommand(self, cmd).Wait() != 0: 2457 if GitCommand(self, cmd).Wait() != 0:
2515 raise GitError('%s merge %s ' % (self.name, head)) 2458 raise GitError('%s merge %s ' % (self.name, head))
2516 2459
2517 def _InitGitDir(self, mirror_git=None, force_sync=False): 2460 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
2518 init_git_dir = not os.path.exists(self.gitdir) 2461 init_git_dir = not os.path.exists(self.gitdir)
2519 init_obj_dir = not os.path.exists(self.objdir) 2462 init_obj_dir = not os.path.exists(self.objdir)
2520 try: 2463 try:
@@ -2523,6 +2466,12 @@ class Project(object):
2523 os.makedirs(self.objdir) 2466 os.makedirs(self.objdir)
2524 self.bare_objdir.init() 2467 self.bare_objdir.init()
2525 2468
2469 if self.use_git_worktrees:
2470 # Enable per-worktree config file support if possible. This is more a
2471 # nice-to-have feature for users rather than a hard requirement.
2472 if git_require((2, 20, 0)):
2473 self.EnableRepositoryExtension('worktreeConfig')
2474
2526 # If we have a separate directory to hold refs, initialize it as well. 2475 # If we have a separate directory to hold refs, initialize it as well.
2527 if self.objdir != self.gitdir: 2476 if self.objdir != self.gitdir:
2528 if init_git_dir: 2477 if init_git_dir:
@@ -2542,8 +2491,9 @@ class Project(object):
2542 if self.worktree and os.path.exists(platform_utils.realpath 2491 if self.worktree and os.path.exists(platform_utils.realpath
2543 (self.worktree)): 2492 (self.worktree)):
2544 platform_utils.rmtree(platform_utils.realpath(self.worktree)) 2493 platform_utils.rmtree(platform_utils.realpath(self.worktree))
2545 return self._InitGitDir(mirror_git=mirror_git, force_sync=False) 2494 return self._InitGitDir(mirror_git=mirror_git, force_sync=False,
2546 except: 2495 quiet=quiet)
2496 except Exception:
2547 raise e 2497 raise e
2548 raise e 2498 raise e
2549 2499
@@ -2556,13 +2506,15 @@ class Project(object):
2556 mirror_git = os.path.join(ref_dir, self.name + '.git') 2506 mirror_git = os.path.join(ref_dir, self.name + '.git')
2557 repo_git = os.path.join(ref_dir, '.repo', 'projects', 2507 repo_git = os.path.join(ref_dir, '.repo', 'projects',
2558 self.relpath + '.git') 2508 self.relpath + '.git')
2509 worktrees_git = os.path.join(ref_dir, '.repo', 'worktrees',
2510 self.name + '.git')
2559 2511
2560 if os.path.exists(mirror_git): 2512 if os.path.exists(mirror_git):
2561 ref_dir = mirror_git 2513 ref_dir = mirror_git
2562
2563 elif os.path.exists(repo_git): 2514 elif os.path.exists(repo_git):
2564 ref_dir = repo_git 2515 ref_dir = repo_git
2565 2516 elif os.path.exists(worktrees_git):
2517 ref_dir = worktrees_git
2566 else: 2518 else:
2567 ref_dir = None 2519 ref_dir = None
2568 2520
@@ -2574,7 +2526,7 @@ class Project(object):
2574 _lwrite(os.path.join(self.gitdir, 'objects/info/alternates'), 2526 _lwrite(os.path.join(self.gitdir, 'objects/info/alternates'),
2575 os.path.join(ref_dir, 'objects') + '\n') 2527 os.path.join(ref_dir, 'objects') + '\n')
2576 2528
2577 self._UpdateHooks() 2529 self._UpdateHooks(quiet=quiet)
2578 2530
2579 m = self.manifest.manifestProject.config 2531 m = self.manifest.manifestProject.config
2580 for key in ['user.name', 'user.email']: 2532 for key in ['user.name', 'user.email']:
@@ -2582,10 +2534,7 @@ class Project(object):
2582 self.config.SetString(key, m.GetString(key)) 2534 self.config.SetString(key, m.GetString(key))
2583 self.config.SetString('filter.lfs.smudge', 'git-lfs smudge --skip -- %f') 2535 self.config.SetString('filter.lfs.smudge', 'git-lfs smudge --skip -- %f')
2584 self.config.SetString('filter.lfs.process', 'git-lfs filter-process --skip') 2536 self.config.SetString('filter.lfs.process', 'git-lfs filter-process --skip')
2585 if self.manifest.IsMirror: 2537 self.config.SetBoolean('core.bare', True if self.manifest.IsMirror else None)
2586 self.config.SetString('core.bare', 'true')
2587 else:
2588 self.config.SetString('core.bare', None)
2589 except Exception: 2538 except Exception:
2590 if init_obj_dir and os.path.exists(self.objdir): 2539 if init_obj_dir and os.path.exists(self.objdir):
2591 platform_utils.rmtree(self.objdir) 2540 platform_utils.rmtree(self.objdir)
@@ -2593,11 +2542,11 @@ class Project(object):
2593 platform_utils.rmtree(self.gitdir) 2542 platform_utils.rmtree(self.gitdir)
2594 raise 2543 raise
2595 2544
2596 def _UpdateHooks(self): 2545 def _UpdateHooks(self, quiet=False):
2597 if os.path.exists(self.gitdir): 2546 if os.path.exists(self.gitdir):
2598 self._InitHooks() 2547 self._InitHooks(quiet=quiet)
2599 2548
2600 def _InitHooks(self): 2549 def _InitHooks(self, quiet=False):
2601 hooks = platform_utils.realpath(self._gitdir_path('hooks')) 2550 hooks = platform_utils.realpath(self._gitdir_path('hooks'))
2602 if not os.path.exists(hooks): 2551 if not os.path.exists(hooks):
2603 os.makedirs(hooks) 2552 os.makedirs(hooks)
@@ -2617,18 +2566,23 @@ class Project(object):
2617 if platform_utils.islink(dst): 2566 if platform_utils.islink(dst):
2618 continue 2567 continue
2619 if os.path.exists(dst): 2568 if os.path.exists(dst):
2620 if filecmp.cmp(stock_hook, dst, shallow=False): 2569 # If the files are the same, we'll leave it alone. We create symlinks
2621 platform_utils.remove(dst) 2570 # below by default but fallback to hardlinks if the OS blocks them.
2622 else: 2571 # So if we're here, it's probably because we made a hardlink below.
2623 _warn("%s: Not replacing locally modified %s hook", 2572 if not filecmp.cmp(stock_hook, dst, shallow=False):
2624 self.relpath, name) 2573 if not quiet:
2625 continue 2574 _warn("%s: Not replacing locally modified %s hook",
2575 self.relpath, name)
2576 continue
2626 try: 2577 try:
2627 platform_utils.symlink( 2578 platform_utils.symlink(
2628 os.path.relpath(stock_hook, os.path.dirname(dst)), dst) 2579 os.path.relpath(stock_hook, os.path.dirname(dst)), dst)
2629 except OSError as e: 2580 except OSError as e:
2630 if e.errno == errno.EPERM: 2581 if e.errno == errno.EPERM:
2631 raise GitError(self._get_symlink_error_message()) 2582 try:
2583 os.link(stock_hook, dst)
2584 except OSError:
2585 raise GitError(self._get_symlink_error_message())
2632 else: 2586 else:
2633 raise 2587 raise
2634 2588
@@ -2648,27 +2602,56 @@ class Project(object):
2648 2602
2649 def _InitMRef(self): 2603 def _InitMRef(self):
2650 if self.manifest.branch: 2604 if self.manifest.branch:
2651 self._InitAnyMRef(R_M + self.manifest.branch) 2605 if self.use_git_worktrees:
2606 # Set up the m/ space to point to the worktree-specific ref space.
2607 # We'll update the worktree-specific ref space on each checkout.
2608 ref = R_M + self.manifest.branch
2609 if not self.bare_ref.symref(ref):
2610 self.bare_git.symbolic_ref(
2611 '-m', 'redirecting to worktree scope',
2612 ref, R_WORKTREE_M + self.manifest.branch)
2613
2614 # We can't update this ref with git worktrees until it exists.
2615 # We'll wait until the initial checkout to set it.
2616 if not os.path.exists(self.worktree):
2617 return
2618
2619 base = R_WORKTREE_M
2620 active_git = self.work_git
2621
2622 self._InitAnyMRef(HEAD, self.bare_git, detach=True)
2623 else:
2624 base = R_M
2625 active_git = self.bare_git
2626
2627 self._InitAnyMRef(base + self.manifest.branch, active_git)
2652 2628
2653 def _InitMirrorHead(self): 2629 def _InitMirrorHead(self):
2654 self._InitAnyMRef(HEAD) 2630 self._InitAnyMRef(HEAD, self.bare_git)
2655 2631
2656 def _InitAnyMRef(self, ref): 2632 def _InitAnyMRef(self, ref, active_git, detach=False):
2657 cur = self.bare_ref.symref(ref) 2633 cur = self.bare_ref.symref(ref)
2658 2634
2659 if self.revisionId: 2635 if self.revisionId:
2660 if cur != '' or self.bare_ref.get(ref) != self.revisionId: 2636 if cur != '' or self.bare_ref.get(ref) != self.revisionId:
2661 msg = 'manifest set to %s' % self.revisionId 2637 msg = 'manifest set to %s' % self.revisionId
2662 dst = self.revisionId + '^0' 2638 dst = self.revisionId + '^0'
2663 self.bare_git.UpdateRef(ref, dst, message=msg, detach=True) 2639 active_git.UpdateRef(ref, dst, message=msg, detach=True)
2664 else: 2640 else:
2665 remote = self.GetRemote(self.remote.name) 2641 remote = self.GetRemote(self.remote.name)
2666 dst = remote.ToLocal(self.revisionExpr) 2642 dst = remote.ToLocal(self.revisionExpr)
2667 if cur != dst: 2643 if cur != dst:
2668 msg = 'manifest set to %s' % self.revisionExpr 2644 msg = 'manifest set to %s' % self.revisionExpr
2669 self.bare_git.symbolic_ref('-m', msg, ref, dst) 2645 if detach:
2646 active_git.UpdateRef(ref, dst, message=msg, detach=True)
2647 else:
2648 active_git.symbolic_ref('-m', msg, ref, dst)
2670 2649
2671 def _CheckDirReference(self, srcdir, destdir, share_refs): 2650 def _CheckDirReference(self, srcdir, destdir, share_refs):
2651 # Git worktrees don't use symlinks to share at all.
2652 if self.use_git_worktrees:
2653 return
2654
2672 symlink_files = self.shareable_files[:] 2655 symlink_files = self.shareable_files[:]
2673 symlink_dirs = self.shareable_dirs[:] 2656 symlink_dirs = self.shareable_dirs[:]
2674 if share_refs: 2657 if share_refs:
@@ -2676,9 +2659,31 @@ class Project(object):
2676 symlink_dirs += self.working_tree_dirs 2659 symlink_dirs += self.working_tree_dirs
2677 to_symlink = symlink_files + symlink_dirs 2660 to_symlink = symlink_files + symlink_dirs
2678 for name in set(to_symlink): 2661 for name in set(to_symlink):
2679 dst = platform_utils.realpath(os.path.join(destdir, name)) 2662 # Try to self-heal a bit in simple cases.
2663 dst_path = os.path.join(destdir, name)
2664 src_path = os.path.join(srcdir, name)
2665
2666 if name in self.working_tree_dirs:
2667 # If the dir is missing under .repo/projects/, create it.
2668 if not os.path.exists(src_path):
2669 os.makedirs(src_path)
2670
2671 elif name in self.working_tree_files:
2672 # If it's a file under the checkout .git/ and the .repo/projects/ has
2673 # nothing, move the file under the .repo/projects/ tree.
2674 if not os.path.exists(src_path) and os.path.isfile(dst_path):
2675 platform_utils.rename(dst_path, src_path)
2676
2677 # If the path exists under the .repo/projects/ and there's no symlink
2678 # under the checkout .git/, recreate the symlink.
2679 if name in self.working_tree_dirs or name in self.working_tree_files:
2680 if os.path.exists(src_path) and not os.path.exists(dst_path):
2681 platform_utils.symlink(
2682 os.path.relpath(src_path, os.path.dirname(dst_path)), dst_path)
2683
2684 dst = platform_utils.realpath(dst_path)
2680 if os.path.lexists(dst): 2685 if os.path.lexists(dst):
2681 src = platform_utils.realpath(os.path.join(srcdir, name)) 2686 src = platform_utils.realpath(src_path)
2682 # Fail if the links are pointing to the wrong place 2687 # Fail if the links are pointing to the wrong place
2683 if src != dst: 2688 if src != dst:
2684 _error('%s is different in %s vs %s', name, destdir, srcdir) 2689 _error('%s is different in %s vs %s', name, destdir, srcdir)
@@ -2735,10 +2740,7 @@ class Project(object):
2735 # If the source file doesn't exist, ensure the destination 2740 # If the source file doesn't exist, ensure the destination
2736 # file doesn't either. 2741 # file doesn't either.
2737 if name in symlink_files and not os.path.lexists(src): 2742 if name in symlink_files and not os.path.lexists(src):
2738 try: 2743 platform_utils.remove(dst, missing_ok=True)
2739 platform_utils.remove(dst)
2740 except OSError:
2741 pass
2742 2744
2743 except OSError as e: 2745 except OSError as e:
2744 if e.errno == errno.EPERM: 2746 if e.errno == errno.EPERM:
@@ -2746,11 +2748,45 @@ class Project(object):
2746 else: 2748 else:
2747 raise 2749 raise
2748 2750
2751 def _InitGitWorktree(self):
2752 """Init the project using git worktrees."""
2753 self.bare_git.worktree('prune')
2754 self.bare_git.worktree('add', '-ff', '--checkout', '--detach', '--lock',
2755 self.worktree, self.GetRevisionId())
2756
2757 # Rewrite the internal state files to use relative paths between the
2758 # checkouts & worktrees.
2759 dotgit = os.path.join(self.worktree, '.git')
2760 with open(dotgit, 'r') as fp:
2761 # Figure out the checkout->worktree path.
2762 setting = fp.read()
2763 assert setting.startswith('gitdir:')
2764 git_worktree_path = setting.split(':', 1)[1].strip()
2765 # Some platforms (e.g. Windows) won't let us update dotgit in situ because
2766 # of file permissions. Delete it and recreate it from scratch to avoid.
2767 platform_utils.remove(dotgit)
2768 # Use relative path from checkout->worktree & maintain Unix line endings
2769 # on all OS's to match git behavior.
2770 with open(dotgit, 'w', newline='\n') as fp:
2771 print('gitdir:', os.path.relpath(git_worktree_path, self.worktree),
2772 file=fp)
2773 # Use relative path from worktree->checkout & maintain Unix line endings
2774 # on all OS's to match git behavior.
2775 with open(os.path.join(git_worktree_path, 'gitdir'), 'w', newline='\n') as fp:
2776 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
2777
2778 self._InitMRef()
2779
2749 def _InitWorkTree(self, force_sync=False, submodules=False): 2780 def _InitWorkTree(self, force_sync=False, submodules=False):
2750 realdotgit = os.path.join(self.worktree, '.git') 2781 realdotgit = os.path.join(self.worktree, '.git')
2751 tmpdotgit = realdotgit + '.tmp' 2782 tmpdotgit = realdotgit + '.tmp'
2752 init_dotgit = not os.path.exists(realdotgit) 2783 init_dotgit = not os.path.exists(realdotgit)
2753 if init_dotgit: 2784 if init_dotgit:
2785 if self.use_git_worktrees:
2786 self._InitGitWorktree()
2787 self._CopyAndLinkFiles()
2788 return
2789
2754 dotgit = tmpdotgit 2790 dotgit = tmpdotgit
2755 platform_utils.rmtree(tmpdotgit, ignore_errors=True) 2791 platform_utils.rmtree(tmpdotgit, ignore_errors=True)
2756 os.makedirs(tmpdotgit) 2792 os.makedirs(tmpdotgit)
@@ -2766,7 +2802,7 @@ class Project(object):
2766 try: 2802 try:
2767 platform_utils.rmtree(dotgit) 2803 platform_utils.rmtree(dotgit)
2768 return self._InitWorkTree(force_sync=False, submodules=submodules) 2804 return self._InitWorkTree(force_sync=False, submodules=submodules)
2769 except: 2805 except Exception:
2770 raise e 2806 raise e
2771 raise e 2807 raise e
2772 2808
@@ -2857,6 +2893,13 @@ class Project(object):
2857 self._bare = bare 2893 self._bare = bare
2858 self._gitdir = gitdir 2894 self._gitdir = gitdir
2859 2895
2896 # __getstate__ and __setstate__ are required for pickling because __getattr__ exists.
2897 def __getstate__(self):
2898 return (self._project, self._bare, self._gitdir)
2899
2900 def __setstate__(self, state):
2901 self._project, self._bare, self._gitdir = state
2902
2860 def LsOthers(self): 2903 def LsOthers(self):
2861 p = GitCommand(self._project, 2904 p = GitCommand(self._project,
2862 ['ls-files', 2905 ['ls-files',
@@ -2885,54 +2928,67 @@ class Project(object):
2885 bare=False, 2928 bare=False,
2886 capture_stdout=True, 2929 capture_stdout=True,
2887 capture_stderr=True) 2930 capture_stderr=True)
2888 try: 2931 p.Wait()
2889 out = p.process.stdout.read() 2932 r = {}
2890 if not hasattr(out, 'encode'): 2933 out = p.stdout
2891 out = out.decode() 2934 if out:
2892 r = {} 2935 out = iter(out[:-1].split('\0'))
2893 if out: 2936 while out:
2894 out = iter(out[:-1].split('\0')) 2937 try:
2895 while out: 2938 info = next(out)
2896 try: 2939 path = next(out)
2897 info = next(out) 2940 except StopIteration:
2898 path = next(out) 2941 break
2899 except StopIteration: 2942
2900 break 2943 class _Info(object):
2901 2944
2902 class _Info(object): 2945 def __init__(self, path, omode, nmode, oid, nid, state):
2903 2946 self.path = path
2904 def __init__(self, path, omode, nmode, oid, nid, state): 2947 self.src_path = None
2905 self.path = path 2948 self.old_mode = omode
2906 self.src_path = None 2949 self.new_mode = nmode
2907 self.old_mode = omode 2950 self.old_id = oid
2908 self.new_mode = nmode 2951 self.new_id = nid
2909 self.old_id = oid 2952
2910 self.new_id = nid 2953 if len(state) == 1:
2911 2954 self.status = state
2912 if len(state) == 1: 2955 self.level = None
2913 self.status = state 2956 else:
2914 self.level = None 2957 self.status = state[:1]
2915 else: 2958 self.level = state[1:]
2916 self.status = state[:1] 2959 while self.level.startswith('0'):
2917 self.level = state[1:] 2960 self.level = self.level[1:]
2918 while self.level.startswith('0'): 2961
2919 self.level = self.level[1:] 2962 info = info[1:].split(' ')
2920 2963 info = _Info(path, *info)
2921 info = info[1:].split(' ') 2964 if info.status in ('R', 'C'):
2922 info = _Info(path, *info) 2965 info.src_path = info.path
2923 if info.status in ('R', 'C'): 2966 info.path = next(out)
2924 info.src_path = info.path 2967 r[info.path] = info
2925 info.path = next(out) 2968 return r
2926 r[info.path] = info 2969
2927 return r 2970 def GetDotgitPath(self, subpath=None):
2928 finally: 2971 """Return the full path to the .git dir.
2929 p.Wait() 2972
2930 2973 As a convenience, append |subpath| if provided.
2931 def GetHead(self): 2974 """
2932 if self._bare: 2975 if self._bare:
2933 path = os.path.join(self._project.gitdir, HEAD) 2976 dotgit = self._gitdir
2934 else: 2977 else:
2935 path = os.path.join(self._project.worktree, '.git', HEAD) 2978 dotgit = os.path.join(self._project.worktree, '.git')
2979 if os.path.isfile(dotgit):
2980 # Git worktrees use a "gitdir:" syntax to point to the scratch space.
2981 with open(dotgit) as fp:
2982 setting = fp.read()
2983 assert setting.startswith('gitdir:')
2984 gitdir = setting.split(':', 1)[1].strip()
2985 dotgit = os.path.normpath(os.path.join(self._project.worktree, gitdir))
2986
2987 return dotgit if subpath is None else os.path.join(dotgit, subpath)
2988
2989 def GetHead(self):
2990 """Return the ref that HEAD points to."""
2991 path = self.GetDotgitPath(subpath=HEAD)
2936 try: 2992 try:
2937 with open(path) as fd: 2993 with open(path) as fd:
2938 line = fd.readline() 2994 line = fd.readline()
@@ -3027,9 +3083,6 @@ class Project(object):
3027 raise TypeError('%s() got an unexpected keyword argument %r' 3083 raise TypeError('%s() got an unexpected keyword argument %r'
3028 % (name, k)) 3084 % (name, k))
3029 if config is not None: 3085 if config is not None:
3030 if not git_require((1, 7, 2)):
3031 raise ValueError('cannot set config on command line for %s()'
3032 % name)
3033 for k, v in config.items(): 3086 for k, v in config.items():
3034 cmdv.append('-c') 3087 cmdv.append('-c')
3035 cmdv.append('%s=%s' % (k, v)) 3088 cmdv.append('%s=%s' % (k, v))
@@ -3109,7 +3162,7 @@ class _Later(object):
3109class _SyncColoring(Coloring): 3162class _SyncColoring(Coloring):
3110 3163
3111 def __init__(self, config): 3164 def __init__(self, config):
3112 Coloring.__init__(self, config, 'reposync') 3165 super().__init__(config, 'reposync')
3113 self.project = self.printer('header', attr='bold') 3166 self.project = self.printer('header', attr='bold')
3114 self.info = self.printer('info') 3167 self.info = self.printer('info')
3115 self.fail = self.printer('fail', fg='red') 3168 self.fail = self.printer('fail', fg='red')
diff --git a/pyversion.py b/pyversion.py
deleted file mode 100644
index f6082408..00000000
--- a/pyversion.py
+++ /dev/null
@@ -1,20 +0,0 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2013 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17import sys
18
19def is_python3():
20 return sys.version_info[0] == 3
diff --git a/release/README.md b/release/README.md
new file mode 100644
index 00000000..3b81d532
--- /dev/null
+++ b/release/README.md
@@ -0,0 +1,2 @@
1These are helper tools for managing official releases.
2See the [release process](../docs/release-process.md) document for more details.
diff --git a/release/sign-launcher.py b/release/sign-launcher.py
new file mode 100755
index 00000000..ba5e490c
--- /dev/null
+++ b/release/sign-launcher.py
@@ -0,0 +1,114 @@
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 launcher scripts correctly.
17
18This is intended to be run only by the official Repo release managers.
19"""
20
21import argparse
22import os
23import subprocess
24import sys
25
26import util
27
28
29def sign(opts):
30 """Sign the launcher!"""
31 output = ''
32 for key in opts.keys:
33 # We use ! at the end of the key so that gpg uses this specific key.
34 # Otherwise it uses the key as a lookup into the overall key and uses the
35 # default signing key. i.e. It will see that KEYID_RSA is a subkey of
36 # another key, and use the primary key to sign instead of the subkey.
37 cmd = ['gpg', '--homedir', opts.gpgdir, '-u', f'{key}!', '--batch', '--yes',
38 '--armor', '--detach-sign', '--output', '-', opts.launcher]
39 ret = util.run(opts, cmd, encoding='utf-8', stdout=subprocess.PIPE)
40 output += ret.stdout
41
42 # Save the combined signatures into one file.
43 with open(f'{opts.launcher}.asc', 'w', encoding='utf-8') as fp:
44 fp.write(output)
45
46
47def check(opts):
48 """Check the signature."""
49 util.run(opts, ['gpg', '--verify', f'{opts.launcher}.asc'])
50
51
52def postmsg(opts):
53 """Helpful info to show at the end for release manager."""
54 print(f"""
55Repo launcher bucket:
56 gs://git-repo-downloads/
57
58To upload this launcher directly:
59 gsutil cp -a public-read {opts.launcher} {opts.launcher}.asc gs://git-repo-downloads/
60
61NB: You probably want to upload it with a specific version first, e.g.:
62 gsutil cp -a public-read {opts.launcher} gs://git-repo-downloads/repo-3.0
63 gsutil cp -a public-read {opts.launcher}.asc gs://git-repo-downloads/repo-3.0.asc
64""")
65
66
67def get_parser():
68 """Get a CLI parser."""
69 parser = argparse.ArgumentParser(description=__doc__)
70 parser.add_argument('-n', '--dry-run',
71 dest='dryrun', action='store_true',
72 help='show everything that would be done')
73 parser.add_argument('--gpgdir',
74 default=os.path.join(util.HOMEDIR, '.gnupg', 'repo'),
75 help='path to dedicated gpg dir with release keys '
76 '(default: ~/.gnupg/repo/)')
77 parser.add_argument('--keyid', dest='keys', default=[], action='append',
78 help='alternative signing keys to use')
79 parser.add_argument('launcher',
80 default=os.path.join(util.TOPDIR, 'repo'), nargs='?',
81 help='the launcher script to sign')
82 return parser
83
84
85def main(argv):
86 """The main func!"""
87 parser = get_parser()
88 opts = parser.parse_args(argv)
89
90 if not os.path.exists(opts.gpgdir):
91 parser.error(f'--gpgdir does not exist: {opts.gpgdir}')
92 if not os.path.exists(opts.launcher):
93 parser.error(f'launcher does not exist: {opts.launcher}')
94
95 opts.launcher = os.path.relpath(opts.launcher)
96 print(f'Signing "{opts.launcher}" launcher script and saving to '
97 f'"{opts.launcher}.asc"')
98
99 if opts.keys:
100 print(f'Using custom keys to sign: {" ".join(opts.keys)}')
101 else:
102 print('Using official Repo release keys to sign')
103 opts.keys = [util.KEYID_DSA, util.KEYID_RSA, util.KEYID_ECC]
104 util.import_release_key(opts)
105
106 sign(opts)
107 check(opts)
108 postmsg(opts)
109
110 return 0
111
112
113if __name__ == '__main__':
114 sys.exit(main(sys.argv[1:]))
diff --git a/release/sign-tag.py b/release/sign-tag.py
new file mode 100755
index 00000000..605437c9
--- /dev/null
+++ b/release/sign-tag.py
@@ -0,0 +1,140 @@
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
18This is intended to be run only by the official Repo release managers, but it
19could be run by people maintaining their own fork of the project.
20
21NB: Check docs/release-process.md for production freeze information.
22"""
23
24import argparse
25import os
26import re
27import subprocess
28import sys
29
30import 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.
35KEYID = util.KEYID_DSA
36
37# Regular expression to validate tag names.
38RE_VALID_TAG = r'^v([0-9]+[.])+[0-9]+$'
39
40
41def 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 = ['git', 'tag', '-s', opts.tag, '-u', f'{opts.key}!',
48 '-m', f'repo {opts.tag}', opts.commit]
49
50 key = 'GNUPGHOME'
51 print('+', f'export {key}="{opts.gpgdir}"')
52 oldvalue = os.getenv(key)
53 os.putenv(key, opts.gpgdir)
54 util.run(opts, cmd)
55 if oldvalue is None:
56 os.unsetenv(key)
57 else:
58 os.putenv(key, oldvalue)
59
60
61def check(opts):
62 """Check the signature."""
63 util.run(opts, ['git', 'tag', '--verify', opts.tag])
64
65
66def postmsg(opts):
67 """Helpful info to show at the end for release manager."""
68 cmd = ['git', 'rev-parse', 'remotes/origin/stable']
69 ret = util.run(opts, cmd, encoding='utf-8', stdout=subprocess.PIPE)
70 current_release = ret.stdout.strip()
71
72 cmd = ['git', 'log', '--format=%h (%aN) %s', '--no-merges',
73 f'remotes/origin/stable..{opts.tag}']
74 ret = util.run(opts, cmd, encoding='utf-8', stdout=subprocess.PIPE)
75 shortlog = ret.stdout.strip()
76
77 print(f"""
78Here's the short log since the last release.
79{shortlog}
80
81To push release to the public:
82 git push origin {opts.commit}:stable {opts.tag} -n
83NB: People will start upgrading to this version immediately.
84
85To roll back a release:
86 git push origin --force {current_release}:stable -n
87""")
88
89
90def get_parser():
91 """Get a CLI parser."""
92 parser = argparse.ArgumentParser(
93 description=__doc__,
94 formatter_class=argparse.RawDescriptionHelpFormatter)
95 parser.add_argument('-n', '--dry-run',
96 dest='dryrun', action='store_true',
97 help='show everything that would be done')
98 parser.add_argument('--gpgdir',
99 default=os.path.join(util.HOMEDIR, '.gnupg', 'repo'),
100 help='path to dedicated gpg dir with release keys '
101 '(default: ~/.gnupg/repo/)')
102 parser.add_argument('-f', '--force', action='store_true',
103 help='force signing of any tag')
104 parser.add_argument('--keyid', dest='key',
105 help='alternative signing key to use')
106 parser.add_argument('tag',
107 help='the tag to create (e.g. "v2.0")')
108 parser.add_argument('commit', default='HEAD', nargs='?',
109 help='the commit to tag')
110 return parser
111
112
113def main(argv):
114 """The main func!"""
115 parser = get_parser()
116 opts = parser.parse_args(argv)
117
118 if not os.path.exists(opts.gpgdir):
119 parser.error(f'--gpgdir does not exist: {opts.gpgdir}')
120
121 if not opts.force and not re.match(RE_VALID_TAG, opts.tag):
122 parser.error(f'tag "{opts.tag}" does not match regex "{RE_VALID_TAG}"; '
123 'use --force to sign anyways')
124
125 if opts.key:
126 print(f'Using custom key to sign: {opts.key}')
127 else:
128 print('Using official Repo release key to sign')
129 opts.key = KEYID
130 util.import_release_key(opts)
131
132 sign(opts)
133 check(opts)
134 postmsg(opts)
135
136 return 0
137
138
139if __name__ == '__main__':
140 sys.exit(main(sys.argv[1:]))
diff --git a/release/update-manpages b/release/update-manpages
new file mode 100755
index 00000000..ddbce0cc
--- /dev/null
+++ b/release/update-manpages
@@ -0,0 +1,102 @@
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
18This is intended to be run before every official Repo release.
19"""
20
21from pathlib import Path
22from functools import partial
23import argparse
24import multiprocessing
25import os
26import re
27import shutil
28import subprocess
29import sys
30import tempfile
31
32TOPDIR = Path(__file__).resolve().parent.parent
33MANDIR = TOPDIR.joinpath('man')
34
35# Load repo local modules.
36sys.path.insert(0, str(TOPDIR))
37from git_command import RepoSourceVersion
38import subcmds
39
40def worker(cmd, **kwargs):
41 subprocess.run(cmd, **kwargs)
42
43def main(argv):
44 parser = argparse.ArgumentParser(description=__doc__)
45 opts = parser.parse_args(argv)
46
47 if not shutil.which('help2man'):
48 sys.exit('Please install help2man to continue.')
49
50 # Let repo know we're generating man pages so it can avoid some dynamic
51 # behavior (like probing active number of CPUs). We use a weird name &
52 # value to make it less likely for users to set this var themselves.
53 os.environ['_REPO_GENERATE_MANPAGES_'] = ' indeed! '
54
55 # "repo branch" is an alias for "repo branches".
56 del subcmds.all_commands['branch']
57 (MANDIR / 'repo-branch.1').write_text('.so man1/repo-branches.1')
58
59 version = RepoSourceVersion()
60 cmdlist = [['help2man', '-N', '-n', f'repo {cmd} - manual page for repo {cmd}',
61 '-S', f'repo {cmd}', '-m', 'Repo Manual', f'--version-string={version}',
62 '-o', MANDIR.joinpath(f'repo-{cmd}.1.tmp'), TOPDIR.joinpath('repo'),
63 '-h', f'help {cmd}'] for cmd in subcmds.all_commands]
64 cmdlist.append(['help2man', '-N', '-n', 'repository management tool built on top of git',
65 '-S', 'repo', '-m', 'Repo Manual', f'--version-string={version}',
66 '-o', MANDIR.joinpath('repo.1.tmp'), TOPDIR.joinpath('repo'),
67 '-h', '--help-all'])
68
69 with tempfile.TemporaryDirectory() as tempdir:
70 repo_dir = Path(tempdir) / '.repo'
71 repo_dir.mkdir()
72 (repo_dir / 'repo').symlink_to(TOPDIR)
73
74 # Run all cmd in parallel, and wait for them to finish.
75 with multiprocessing.Pool() as pool:
76 pool.map(partial(worker, cwd=tempdir, check=True), cmdlist)
77
78 regex = (
79 (r'(It was generated by help2man) [0-9.]+', '\g<1>.'),
80 (r'^\.IP\n(.*:)\n', '.SS \g<1>\n'),
81 (r'^\.PP\nDescription', '.SH DETAILS'),
82 )
83 for tmp_path in MANDIR.glob('*.1.tmp'):
84 path = tmp_path.parent / tmp_path.stem
85 old_data = path.read_text() if path.exists() else ''
86
87 data = tmp_path.read_text()
88 tmp_path.unlink()
89
90 for pattern, replacement in regex:
91 data = re.sub(pattern, replacement, data, flags=re.M)
92
93 # If the only thing that changed was the date, don't refresh. This avoids
94 # a lot of noise when only one file actually updates.
95 old_data = re.sub(r'^(\.TH REPO "1" ")([^"]+)', r'\1', old_data, flags=re.M)
96 new_data = re.sub(r'^(\.TH REPO "1" ")([^"]+)', r'\1', data, flags=re.M)
97 if old_data != new_data:
98 path.write_text(data)
99
100
101if __name__ == '__main__':
102 sys.exit(main(sys.argv[1:]))
diff --git a/release/util.py b/release/util.py
new file mode 100644
index 00000000..9d0eb1dc
--- /dev/null
+++ b/release/util.py
@@ -0,0 +1,73 @@
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
17import os
18import re
19import subprocess
20import sys
21
22
23assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
24
25
26TOPDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
27HOMEDIR = os.path.expanduser('~')
28
29
30# These are the release keys we sign with.
31KEYID_DSA = '8BB9AD793E8E6153AF0F9A4416530D5E920F5C65'
32KEYID_RSA = 'A34A13BE8E76BFF46A0C022DA2E75A824AAB9624'
33KEYID_ECC = 'E1F9040D7A3F6DAFAC897CD3D3B95DA243E48A39'
34
35
36def cmdstr(cmd):
37 """Get a nicely quoted shell command."""
38 ret = []
39 for arg in cmd:
40 if not re.match(r'^[a-zA-Z0-9/_.=-]+$', arg):
41 arg = f'"{arg}"'
42 ret.append(arg)
43 return ' '.join(ret)
44
45
46def run(opts, cmd, check=True, **kwargs):
47 """Helper around subprocess.run to include logging."""
48 print('+', cmdstr(cmd))
49 if opts.dryrun:
50 cmd = ['true', '--'] + cmd
51 try:
52 return subprocess.run(cmd, check=check, **kwargs)
53 except subprocess.CalledProcessError as e:
54 print(f'aborting: {e}', file=sys.stderr)
55 sys.exit(1)
56
57
58def import_release_key(opts):
59 """Import the public key of the official release repo signing key."""
60 # Extract the key from our repo launcher.
61 launcher = getattr(opts, 'launcher', os.path.join(TOPDIR, 'repo'))
62 print(f'Importing keys from "{launcher}" launcher script')
63 with open(launcher, encoding='utf-8') as fp:
64 data = fp.read()
65
66 keys = re.findall(
67 r'\n-----BEGIN PGP PUBLIC KEY BLOCK-----\n[^-]*'
68 r'\n-----END PGP PUBLIC KEY BLOCK-----\n', data, flags=re.M)
69 run(opts, ['gpg', '--import'], input='\n'.join(keys).encode('utf-8'))
70
71 print('Marking keys as fully trusted')
72 run(opts, ['gpg', '--import-ownertrust'],
73 input=f'{KEYID_DSA}:6:\n'.encode('utf-8'))
diff --git a/repo b/repo
index 15d2ff85..4cddbf1e 100755
--- a/repo
+++ b/repo
@@ -1,5 +1,19 @@
1#!/usr/bin/env python 1#!/usr/bin/env python
2# -*- coding:utf-8 -*- 2# -*- coding:utf-8 -*-
3#
4# Copyright (C) 2008 The Android Open Source Project
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
3 17
4"""Repo launcher. 18"""Repo launcher.
5 19
@@ -10,35 +24,135 @@ copy of repo in the checkout.
10 24
11from __future__ import print_function 25from __future__ import print_function
12 26
27import datetime
28import os
29import platform
30import shlex
31import subprocess
32import sys
33
34
35# These should never be newer than the main.py version since this needs to be a
36# bit more flexible with older systems. See that file for more details on the
37# versions we select.
38MIN_PYTHON_VERSION_SOFT = (3, 6)
39MIN_PYTHON_VERSION_HARD = (3, 5)
40
41
42# Keep basic logic in sync with repo_trace.py.
43class Trace(object):
44 """Trace helper logic."""
45
46 REPO_TRACE = 'REPO_TRACE'
47
48 def __init__(self):
49 self.set(os.environ.get(self.REPO_TRACE) == '1')
50
51 def set(self, value):
52 self.enabled = bool(value)
53
54 def print(self, *args, **kwargs):
55 if self.enabled:
56 print(*args, **kwargs)
57
58
59trace = Trace()
60
61
62def exec_command(cmd):
63 """Execute |cmd| or return None on failure."""
64 trace.print(':', ' '.join(cmd))
65 try:
66 if platform.system() == 'Windows':
67 ret = subprocess.call(cmd)
68 sys.exit(ret)
69 else:
70 os.execvp(cmd[0], cmd)
71 except Exception:
72 pass
73
74
75def check_python_version():
76 """Make sure the active Python version is recent enough."""
77 def reexec(prog):
78 exec_command([prog] + sys.argv)
79
80 ver = sys.version_info
81 major = ver.major
82 minor = ver.minor
83
84 # Abort on very old Python 2 versions.
85 if (major, minor) < (2, 7):
86 print('repo: error: Your Python version is too old. '
87 'Please use Python {}.{} or newer instead.'.format(
88 *MIN_PYTHON_VERSION_SOFT), file=sys.stderr)
89 sys.exit(1)
90
91 # Try to re-exec the version specific Python 3 if needed.
92 if (major, minor) < MIN_PYTHON_VERSION_SOFT:
93 # Python makes releases ~once a year, so try our min version +10 to help
94 # bridge the gap. This is the fallback anyways so perf isn't critical.
95 min_major, min_minor = MIN_PYTHON_VERSION_SOFT
96 for inc in range(0, 10):
97 reexec('python{}.{}'.format(min_major, min_minor + inc))
98
99 # Fallback to older versions if possible.
100 for inc in range(MIN_PYTHON_VERSION_SOFT[1] - MIN_PYTHON_VERSION_HARD[1], 0, -1):
101 # Don't downgrade, and don't reexec ourselves (which would infinite loop).
102 if (min_major, min_minor - inc) <= (major, minor):
103 break
104 reexec('python{}.{}'.format(min_major, min_minor - inc))
105
106 # Try the generic Python 3 wrapper, but only if it's new enough. If it
107 # isn't, we want to just give up below and make the user resolve things.
108 try:
109 proc = subprocess.Popen(
110 ['python3', '-c', 'import sys; '
111 'print(sys.version_info.major, sys.version_info.minor)'],
112 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
113 (output, _) = proc.communicate()
114 python3_ver = tuple(int(x) for x in output.decode('utf-8').split())
115 except (OSError, subprocess.CalledProcessError):
116 python3_ver = None
117
118 # If the python3 version looks like it's new enough, give it a try.
119 if (python3_ver and python3_ver >= MIN_PYTHON_VERSION_HARD
120 and python3_ver != (major, minor)):
121 reexec('python3')
122
123 # We're still here, so diagnose things for the user.
124 if major < 3:
125 print('repo: error: Python 2 is no longer supported; '
126 'Please upgrade to Python {}.{}+.'.format(*MIN_PYTHON_VERSION_HARD),
127 file=sys.stderr)
128 sys.exit(1)
129 elif (major, minor) < MIN_PYTHON_VERSION_HARD:
130 print('repo: error: Python 3 version is too old; '
131 'Please use Python {}.{} or newer.'.format(*MIN_PYTHON_VERSION_HARD),
132 file=sys.stderr)
133 sys.exit(1)
134
135
136if __name__ == '__main__':
137 check_python_version()
138
139
13# repo default configuration 140# repo default configuration
14# 141#
15import os
16REPO_URL = os.environ.get('REPO_URL', None) 142REPO_URL = os.environ.get('REPO_URL', None)
17if not REPO_URL: 143if not REPO_URL:
18 REPO_URL = 'https://gerrit.googlesource.com/git-repo' 144 REPO_URL = 'https://gerrit.googlesource.com/git-repo'
19REPO_REV = os.environ.get('REPO_REV') 145REPO_REV = os.environ.get('REPO_REV')
20if not REPO_REV: 146if not REPO_REV:
21 REPO_REV = 'repo-1' 147 REPO_REV = 'stable'
22 148# URL to file bug reports for repo tool issues.
23# Copyright (C) 2008 Google Inc. 149BUG_URL = 'https://bugs.chromium.org/p/gerrit/issues/entry?template=Repo+tool+issue'
24#
25# Licensed under the Apache License, Version 2.0 (the "License");
26# you may not use this file except in compliance with the License.
27# You may obtain a copy of the License at
28#
29# http://www.apache.org/licenses/LICENSE-2.0
30#
31# Unless required by applicable law or agreed to in writing, software
32# distributed under the License is distributed on an "AS IS" BASIS,
33# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
34# See the License for the specific language governing permissions and
35# limitations under the License.
36 150
37# increment this whenever we make important changes to this script 151# increment this whenever we make important changes to this script
38VERSION = (1, 27) 152VERSION = (2, 17)
39 153
40# increment this if the MAINTAINER_KEYS block is modified 154# increment this if the MAINTAINER_KEYS block is modified
41KEYRING_VERSION = (1, 2) 155KEYRING_VERSION = (2, 3)
42 156
43# Each individual key entry is created by using: 157# Each individual key entry is created by using:
44# gpg --armor --export keyid 158# gpg --armor --export keyid
@@ -46,7 +160,6 @@ MAINTAINER_KEYS = """
46 160
47 Repo Maintainer <repo@android.kernel.org> 161 Repo Maintainer <repo@android.kernel.org>
48-----BEGIN PGP PUBLIC KEY BLOCK----- 162-----BEGIN PGP PUBLIC KEY BLOCK-----
49Version: GnuPG v1.4.2.2 (GNU/Linux)
50 163
51mQGiBEj3ugERBACrLJh/ZPyVSKeClMuznFIrsQ+hpNnmJGw1a9GXKYKk8qHPhAZf 164mQGiBEj3ugERBACrLJh/ZPyVSKeClMuznFIrsQ+hpNnmJGw1a9GXKYKk8qHPhAZf
52WKtrBqAVMNRLhL85oSlekRz98u41H5si5zcuv+IXJDF5MJYcB8f22wAy15lUqPWi 165WKtrBqAVMNRLhL85oSlekRz98u41H5si5zcuv+IXJDF5MJYcB8f22wAy15lUqPWi
@@ -82,63 +195,64 @@ p3v5ILwfC7hVx4jHSnOgZ65L9s8EQdVr1ckN9243yta7rNgwfcqb60ILMFF1BRk/
825xGrFy8tfAaeBMIQ17gvFSp/suc9DYO0ICK2BISzq+F+ZiAKsjMYOBNdH/h0zobQ 1955xGrFy8tfAaeBMIQ17gvFSp/suc9DYO0ICK2BISzq+F+ZiAKsjMYOBNdH/h0zobQ
83HTHs37+/QLMomGEGKZMWi0dShU2J5mNRQu3Hhxl3hHDVbt5CeJBb26aQcQrFz69W 196HTHs37+/QLMomGEGKZMWi0dShU2J5mNRQu3Hhxl3hHDVbt5CeJBb26aQcQrFz69W
84zE3GNvmJosh6leayjtI9P2A6iEkEGBECAAkFAkj3uiACGwwACgkQFlMNXpIPXGWp 197zE3GNvmJosh6leayjtI9P2A6iEkEGBECAAkFAkj3uiACGwwACgkQFlMNXpIPXGWp
85TACbBS+Up3RpfYVfd63c1cDdlru13pQAn3NQy/SN858MkxN+zym86UBgOad2 198TACbBS+Up3RpfYVfd63c1cDdlru13pQAn3NQy/SN858MkxN+zym86UBgOad2uQIN
86=CMiZ 199BF5FqOoBEAC8aRtWEtXzeuoQhdFrLTqYs2dy6kl9y+j3DMQYAMs8je582qzUigIO
87-----END PGP PUBLIC KEY BLOCK----- 200ZZxq7T/3WQgghsdw9yPvdzlw9tKdet2TJkR1mtBfSjZQrkKwR0pQP4AD7t/90Whu
88 201R8Wlu8ysapE2hLxMH5Y2znRQX2LkUYmk0K2ik9AgZEh3AFEg3YLl2pGnSjeSp3ch
89 Conley Owens <cco3@android.com> 202cLX2n/rVZf5LXluZGRG+iov1Ka+8m+UqzohMA1DYNECJW6KPgXsNX++i8/iwZVic
90-----BEGIN PGP PUBLIC KEY BLOCK----- 203PWzhRJSQC+QiAZNsKT6HNNKs97YCUVzhjBLnRSxRBPkr0hS/VMWY2V4pbASljWyd
91Version: GnuPG v1.4.11 (GNU/Linux) 204GYmlDcxheLne0yjes0bJAdvig5rB42FOV0FCM4bDYOVwKfZ7SpzGCYXxtlwe0XNG
92 205tLW9WA6tICVqNZ/JNiRTBLrsGSkyrEhDPKnIHlHRI5Zux6IHwMVB0lQKHjSop+t6
93mQENBFHRvc8BCADFg45Xx/y6QDC+T7Y/gGc7vx0ww7qfOwIKlAZ9xG3qKunMxo+S 206oyubqWcPCGGYdz2QGQHNz7huC/Zn0wS4hsoiSwPv6HCq3jNyUkOJ7wZ3ouv60p2I
94hPCnzEl3cq+6I1Ww/ndop/HB3N3toPXRCoN8Vs4/Hc7by+SnaLFnacrm+tV5/OgT 207kPurgviVaRaPSKTYdKfkcJOtFeqOh1na5IHkXsD9rNctB7tSgfsm0G6qJIVe3ZmJ
95V37Lzt8lhay1Kl+YfpFwHYYpIEBLFV9knyfRXS/428W2qhdzYfvB15/AasRmwmor 2087QAyHBfuLrAWCq5xS8EHDlvxPdAD8EEsa9T32YxcHKIkxr1eSwrUrKb8cPhWq1pp
96py4NIzSs8UD/SPr1ihqNCdZM76+MQyN5HMYXW/ALZXUFG0pwluHFA7hrfPG74i8C 209Jiylw6G1fZ02VKixqmPC4oFMyg1PO8L2tcQTrnVmZvfFGiaekHKdhQARAQABiQKW
97zMiP7qvMWIl/r/jtzHioH1dRKgbod+LZsrDJ8mBaqsZaDmNJMhss9g76XvfMyLra 210BBgRAgAgFiEEi7mteT6OYVOvD5pEFlMNXpIPXGUFAl5FqOoCGwICQAkQFlMNXpIP
989DI9/iFuBpGzeqBv0hwOGQspLRrEoyTeR6n1ABEBAAG0H0NvbmxleSBPd2VucyA8 211XGXBdCAEGQEKAB0WIQSjShO+jna/9GoMAi2i51qCSquWJAUCXkWo6gAKCRCi51qC
99Y2NvM0BhbmRyb2lkLmNvbT6JATgEEwECACIFAlHRvc8CGwMGCwkIBwMCBhUIAgkK 212SquWJLzgD/0YEZYS7yKxhP+kk94TcTYMBMSZpU5KFClB77yu4SI1LeXq4ocBT4sp
100CwQWAgMBAh4BAheAAAoJEGe35EhpKzgsP6AIAJKJmNtn4l7hkYHKHFSo3egb6RjQ 213EPaOsQiIx//j59J67b7CBe4UeRA6D2n0pw+bCKuc731DFi5X9C1zq3a7E67SQ2yd
101zEIP3MFTcu8HFX1kF1ZFbrp7xqurLaE53kEkKuAAvjJDAgI8mcZHP1JyplubqjQA 214FbYE2fnpVnMqb62g4sTh7JmdxEtXCWBUWL0OEoWouBW1PkFDHx2kYLC7YpZt3+4t
102xvv84gK+OGP3Xk+QK1ZjUQSbjOpjEiSZpRhWcHci3dgOUH4blJfByHw25hlgHowd 215VtNhSfV8NS6PF8ep3JXHVd2wsC3DQtggeId5GM44o8N0SkwQHNjK8ZD+VZ74ZnhZ
103a/2PrNKZVcJ92YienaxxGjcXEUcd0uYEG2+rwllQigFcnMFDhr9B71MfalRHjFKE 216HeyHskomiOC61LrZWQvxD6VqtfnBQ5GvONO8QuhkiFwMMOnpPVj2k7ngSkd5o27K
104fmdoypqLrri61YBc59P88Rw2/WUpTQjgNubSqa3A2+CKdaRyaRw+2fdF4TdR0h8W 2176c53ZESOlR4bAfl0i3RZYC9B5KerGkBE3dTgTzmGjOaahl2eLz4LDPdTwMtS+sAU
105zbg+lbaPtJHsV+3mJC7fq26MiJDRJa5ZztpMn8su20gbLgi2ShBOaHAYDDi5AQ0E 2181hPPvZTQeYDdV62bOWUyteMoJu354GgZPQ9eItWYixpNCyOGNcJXl6xk3/OuoP6f
106UdG9zwEIAMoOBq+QLNozAhxOOl5GL3StTStGRgPRXINfmViTsihrqGCWBBUfXlUE 219MciFV8aMxs/7mUR8q1Ei3X9MKu+bbODYj2rC1tMkLj1OaAJkfvRuYrKsQpoUsn4q
107OytC0mYcrDUQev/8ToVoyqw+iGSwDkcSXkrEUCKFtHV/GECWtk1keyHgR10YKI1R 220VT9+aciNpU/I7M30watlWo7RfUFI3zaGdMDcMFju1cWt2Un8E3gtscGufzbz1Z5Z
108mquSXoubWGqPeG1PAI74XWaRx8UrL8uCXUtmD8Q5J7mDjKR5NpxaXrwlA0bKsf2E 221Gak+tCOWUyuYNWX3noit7Dk6+3JGHGaQettldNu2PLM9SbIXd2EaqK/eEv9BS3dd
109Gp9tu1kKauuToZhWHMRMqYSOGikQJwWSFYKT1KdNcOXLQF6+bfoJ6sjVYdwfmNQL 222ItkZwzyZXSaQ9UqAceY1AHskJJ5KVXIRLuhP5jBWWo3fnRMyMYt2nwNBAJ9B9TA8
110Ixn8QVhoTDedcqClSWB17VDEFDFa7MmqXZz2qtM3X1R/MUMHqPtegQzBGNhRdnI2 223VlBniwIl5EzCvOFOTGrtewCdHOvr3N3ieypGz1BzyCN9tJMO3G24MwReRal9Fgkr
111V45+1Nnx/uuCxDbeI4RbHzujnxDiq70AEQEAAYkBHwQYAQIACQUCUdG9zwIbDAAK 224BgEEAdpHDwEBB0BhPE/je6OuKgWzJ1mnrUmHhn4IMOHp+58+T5kHU3Oy6YjXBBgR
112CRBnt+RIaSs4LNVeB/0Y2pZ8I7gAAcEM0Xw8drr4omg2fUoK1J33ozlA/RxeA/lJ 225AgAgFiEEi7mteT6OYVOvD5pEFlMNXpIPXGUFAl5FqX0CGwIAgQkQFlMNXpIPXGV2
113I3KnyCDTpXuIeBKPGkdL8uMATC9Z8DnBBajRlftNDVZS3Hz4G09G9QpMojvJkFJV 226IAQZFggAHRYhBOH5BA16P22vrIl809O5XaJD5Io5BQJeRal9AAoJENO5XaJD5Io5
114By+01Flw/X+eeN8NpqSuLV4W+AjEO8at/VvgKr1AFvBRdZ7GkpI1o6DgPe7ZqX+1 227MEkA/3uLmiwANOcgE0zB9zga0T/KkYhYOWFx7zRyDhrTf9spAPwIfSBOAGtwxjLO
115dzQZt3e13W0rVBb/bUgx9iSLoeWP3aq/k+/GRGOR+S6F6BBSl0SQ2EF2+dIywb1x 228DCce5OaQJl/YuGHvXq2yx5h7T8pdAZ+PAJ4qfIk2LLSidsplTDXOKhOQAuOqUQCf
116JuinEP+AwLAUZ1Bsx9ISC0Agpk2VeHXPL3FGhroEmoMvBzO0kTFGyoeT7PR/BfKv 229cZ7aFsJF4PtcDrfdejyAxbtsSHI=
117+H/g3HsL2LOB9uoIm8/5p2TTU5ttYCXMHhQZ81AY 230=82Tj
118=AUp4
119-----END PGP PUBLIC KEY BLOCK----- 231-----END PGP PUBLIC KEY BLOCK-----
120""" 232"""
121 233
122GIT = 'git' # our git command 234GIT = 'git' # our git command
235# NB: The version of git that the repo launcher requires may be much older than
236# the version of git that the main repo source tree requires. Keeping this at
237# an older version also makes it easier for users to upgrade/rollback as needed.
238#
239# git-1.7 is in (EOL) Ubuntu Precise.
123MIN_GIT_VERSION = (1, 7, 2) # minimum supported git version 240MIN_GIT_VERSION = (1, 7, 2) # minimum supported git version
124repodir = '.repo' # name of repo's private directory 241repodir = '.repo' # name of repo's private directory
125S_repo = 'repo' # special repo repository 242S_repo = 'repo' # special repo repository
126S_manifests = 'manifests' # special manifest repository 243S_manifests = 'manifests' # special manifest repository
127REPO_MAIN = S_repo + '/main.py' # main script 244REPO_MAIN = S_repo + '/main.py' # main script
128MIN_PYTHON_VERSION = (2, 7) # minimum supported python version
129GITC_CONFIG_FILE = '/gitc/.config' 245GITC_CONFIG_FILE = '/gitc/.config'
130GITC_FS_ROOT_DIR = '/gitc/manifest-rw/' 246GITC_FS_ROOT_DIR = '/gitc/manifest-rw/'
131 247
132 248
133import collections 249import collections
134import errno 250import errno
251import json
135import optparse 252import optparse
136import platform
137import re 253import re
138import shutil 254import shutil
139import stat 255import stat
140import subprocess
141import sys
142 256
143if sys.version_info[0] == 3: 257if sys.version_info[0] == 3:
144 import urllib.request 258 import urllib.request
@@ -151,116 +265,215 @@ else:
151 urllib.error = urllib2 265 urllib.error = urllib2
152 266
153 267
154# Python version check
155ver = sys.version_info
156if (ver[0], ver[1]) < MIN_PYTHON_VERSION:
157 print('error: Python version {} unsupported.\n'
158 'Please use Python {}.{} instead.'.format(
159 sys.version.split(' ')[0],
160 MIN_PYTHON_VERSION[0],
161 MIN_PYTHON_VERSION[1],
162 ), file=sys.stderr)
163 sys.exit(1)
164
165home_dot_repo = os.path.expanduser('~/.repoconfig') 268home_dot_repo = os.path.expanduser('~/.repoconfig')
166gpg_dir = os.path.join(home_dot_repo, 'gnupg') 269gpg_dir = os.path.join(home_dot_repo, 'gnupg')
167 270
168extra_args = [] 271
169init_optparse = optparse.OptionParser(usage="repo init -u url [options]") 272def GetParser(gitc_init=False):
170 273 """Setup the CLI parser."""
171# Logging 274 if gitc_init:
172group = init_optparse.add_option_group('Logging options') 275 usage = 'repo gitc-init -c client [options] [-u] url'
173group.add_option('-q', '--quiet', 276 else:
174 dest="quiet", action="store_true", default=False, 277 usage = 'repo init [options] [-u] url'
175 help="be quiet") 278
176 279 parser = optparse.OptionParser(usage=usage)
177# Manifest 280 InitParser(parser, gitc_init=gitc_init)
178group = init_optparse.add_option_group('Manifest options') 281 return parser
179group.add_option('-u', '--manifest-url', 282
180 dest='manifest_url', 283
181 help='manifest repository location', metavar='URL') 284def InitParser(parser, gitc_init=False):
182group.add_option('-b', '--manifest-branch', 285 """Setup the CLI parser."""
183 dest='manifest_branch', 286 # NB: Keep in sync with command.py:_CommonOptions().
184 help='manifest branch or revision', metavar='REVISION') 287
185group.add_option('-m', '--manifest-name', 288 # Logging.
186 dest='manifest_name', 289 group = parser.add_option_group('Logging options')
187 help='initial manifest file', metavar='NAME.xml') 290 group.add_option('-v', '--verbose',
188group.add_option('--current-branch', 291 dest='output_mode', action='store_true',
189 dest='current_branch_only', action='store_true', 292 help='show all output')
190 help='fetch only current manifest branch from server') 293 group.add_option('-q', '--quiet',
191group.add_option('--mirror', 294 dest='output_mode', action='store_false',
192 dest='mirror', action='store_true', 295 help='only show errors')
193 help='create a replica of the remote repositories ' 296
194 'rather than a client working directory') 297 # Manifest.
195group.add_option('--reference', 298 group = parser.add_option_group('Manifest options')
196 dest='reference', 299 group.add_option('-u', '--manifest-url',
197 help='location of mirror directory', metavar='DIR') 300 help='manifest repository location', metavar='URL')
198group.add_option('--dissociate', 301 group.add_option('-b', '--manifest-branch', metavar='REVISION',
199 dest='dissociate', action='store_true', 302 help='manifest branch or revision (use HEAD for default)')
200 help='dissociate from reference mirrors after clone') 303 group.add_option('-m', '--manifest-name', default='default.xml',
201group.add_option('--depth', type='int', default=None, 304 help='initial manifest file', metavar='NAME.xml')
202 dest='depth', 305 group.add_option('-g', '--groups', default='default',
203 help='create a shallow clone with given depth; see git clone') 306 help='restrict manifest projects to ones with specified '
204group.add_option('--partial-clone', action='store_true', 307 'group(s) [default|all|G1,G2,G3|G4,-G5,-G6]',
205 dest='partial_clone', 308 metavar='GROUP')
206 help='perform partial clone (https://git-scm.com/' 309 group.add_option('-p', '--platform', default='auto',
207 'docs/gitrepository-layout#_code_partialclone_code)') 310 help='restrict manifest projects to ones with a specified '
208group.add_option('--clone-filter', action='store', default='blob:none', 311 'platform group [auto|all|none|linux|darwin|...]',
209 dest='clone_filter', 312 metavar='PLATFORM')
210 help='filter for use with --partial-clone [default: %default]') 313 group.add_option('--submodules', action='store_true',
211group.add_option('--archive', 314 help='sync any submodules associated with the manifest repo')
212 dest='archive', action='store_true', 315 group.add_option('--standalone-manifest', action='store_true',
213 help='checkout an archive instead of a git repository for ' 316 help='download the manifest as a static file '
214 'each project. See git archive.') 317 'rather then create a git checkout of '
215group.add_option('--submodules', 318 'the manifest repo')
216 dest='submodules', action='store_true', 319
217 help='sync any submodules associated with the manifest repo') 320 # Options that only affect manifest project, and not any of the projects
218group.add_option('-g', '--groups', 321 # specified in the manifest itself.
219 dest='groups', default='default', 322 group = parser.add_option_group('Manifest (only) checkout options')
220 help='restrict manifest projects to ones with specified ' 323 cbr_opts = ['--current-branch']
221 'group(s) [default|all|G1,G2,G3|G4,-G5,-G6]', 324 # The gitc-init subcommand allocates -c itself, but a lot of init users
222 metavar='GROUP') 325 # want -c, so try to satisfy both as best we can.
223group.add_option('-p', '--platform', 326 if not gitc_init:
224 dest='platform', default="auto", 327 cbr_opts += ['-c']
225 help='restrict manifest projects to ones with a specified ' 328 group.add_option(*cbr_opts,
226 'platform group [auto|all|none|linux|darwin|...]', 329 dest='current_branch_only', action='store_true',
227 metavar='PLATFORM') 330 help='fetch only current manifest branch from server')
228group.add_option('--no-clone-bundle', 331 group.add_option('--no-current-branch',
229 dest='no_clone_bundle', action='store_true', 332 dest='current_branch_only', action='store_false',
230 help='disable use of /clone.bundle on HTTP/HTTPS') 333 help='fetch all manifest branches from server')
231group.add_option('--no-tags', 334 group.add_option('--tags',
232 dest='no_tags', action='store_true', 335 action='store_true',
233 help="don't fetch tags in the manifest") 336 help='fetch tags in the manifest')
234 337 group.add_option('--no-tags',
235 338 dest='tags', action='store_false',
236# Tool 339 help="don't fetch tags in the manifest")
237group = init_optparse.add_option_group('repo Version options') 340
238group.add_option('--repo-url', 341 # These are fundamentally different ways of structuring the checkout.
239 dest='repo_url', 342 group = parser.add_option_group('Checkout modes')
240 help='repo repository location ($REPO_URL)', metavar='URL') 343 group.add_option('--mirror', action='store_true',
241group.add_option('--repo-branch', 344 help='create a replica of the remote repositories '
242 dest='repo_branch', 345 'rather than a client working directory')
243 help='repo branch or revision ($REPO_REV)', metavar='REVISION') 346 group.add_option('--archive', action='store_true',
244group.add_option('--no-repo-verify', 347 help='checkout an archive instead of a git repository for '
245 dest='no_repo_verify', action='store_true', 348 'each project. See git archive.')
246 help='do not verify repo source code') 349 group.add_option('--worktree', action='store_true',
247 350 help='use git-worktree to manage projects')
248# Other 351
249group = init_optparse.add_option_group('Other options') 352 # These are fundamentally different ways of structuring the checkout.
250group.add_option('--config-name', 353 group = parser.add_option_group('Project checkout optimizations')
251 dest='config_name', action="store_true", default=False, 354 group.add_option('--reference',
252 help='Always prompt for name/e-mail') 355 help='location of mirror directory', metavar='DIR')
253 356 group.add_option('--dissociate', action='store_true',
254 357 help='dissociate from reference mirrors after clone')
255def _GitcInitOptions(init_optparse_arg): 358 group.add_option('--depth', type='int', default=None,
256 init_optparse_arg.set_usage("repo gitc-init -u url -c client [options]") 359 help='create a shallow clone with given depth; '
257 g = init_optparse_arg.add_option_group('GITC options') 360 'see git clone')
258 g.add_option('-f', '--manifest-file', 361 group.add_option('--partial-clone', action='store_true',
259 dest='manifest_file', 362 help='perform partial clone (https://git-scm.com/'
260 help='Optional manifest file to use for this GITC client.') 363 'docs/gitrepository-layout#_code_partialclone_code)')
261 g.add_option('-c', '--gitc-client', 364 group.add_option('--no-partial-clone', action='store_false',
262 dest='gitc_client', 365 help='disable use of partial clone (https://git-scm.com/'
263 help='The name of the gitc_client instance to create or modify.') 366 'docs/gitrepository-layout#_code_partialclone_code)')
367 group.add_option('--partial-clone-exclude', action='store',
368 help='exclude the specified projects (a comma-delimited '
369 'project names) from partial clone (https://git-scm.com'
370 '/docs/gitrepository-layout#_code_partialclone_code)')
371 group.add_option('--clone-filter', action='store', default='blob:none',
372 help='filter for use with --partial-clone '
373 '[default: %default]')
374 group.add_option('--use-superproject', action='store_true', default=None,
375 help='use the manifest superproject to sync projects')
376 group.add_option('--no-use-superproject', action='store_false',
377 dest='use_superproject',
378 help='disable use of manifest superprojects')
379 group.add_option('--clone-bundle', action='store_true',
380 help='enable use of /clone.bundle on HTTP/HTTPS '
381 '(default if not --partial-clone)')
382 group.add_option('--no-clone-bundle',
383 dest='clone_bundle', action='store_false',
384 help='disable use of /clone.bundle on HTTP/HTTPS (default if --partial-clone)')
385
386 # Tool.
387 group = parser.add_option_group('repo Version options')
388 group.add_option('--repo-url', metavar='URL',
389 help='repo repository location ($REPO_URL)')
390 group.add_option('--repo-rev', metavar='REV',
391 help='repo branch or revision ($REPO_REV)')
392 group.add_option('--repo-branch', dest='repo_rev',
393 help=optparse.SUPPRESS_HELP)
394 group.add_option('--no-repo-verify',
395 dest='repo_verify', default=True, action='store_false',
396 help='do not verify repo source code')
397
398 # Other.
399 group = parser.add_option_group('Other options')
400 group.add_option('--config-name',
401 action='store_true', default=False,
402 help='Always prompt for name/e-mail')
403
404 # gitc-init specific settings.
405 if gitc_init:
406 group = parser.add_option_group('GITC options')
407 group.add_option('-f', '--manifest-file',
408 help='Optional manifest file to use for this GITC client.')
409 group.add_option('-c', '--gitc-client',
410 help='Name of the gitc_client instance to create or modify.')
411
412 return parser
413
414
415# This is a poor replacement for subprocess.run until we require Python 3.6+.
416RunResult = collections.namedtuple(
417 'RunResult', ('returncode', 'stdout', 'stderr'))
418
419
420class RunError(Exception):
421 """Error when running a command failed."""
422
423
424def run_command(cmd, **kwargs):
425 """Run |cmd| and return its output."""
426 check = kwargs.pop('check', False)
427 if kwargs.pop('capture_output', False):
428 kwargs.setdefault('stdout', subprocess.PIPE)
429 kwargs.setdefault('stderr', subprocess.PIPE)
430 cmd_input = kwargs.pop('input', None)
431
432 def decode(output):
433 """Decode |output| to text."""
434 if output is None:
435 return output
436 try:
437 return output.decode('utf-8')
438 except UnicodeError:
439 print('repo: warning: Invalid UTF-8 output:\ncmd: %r\n%r' % (cmd, output),
440 file=sys.stderr)
441 # TODO(vapier): Once we require Python 3, use 'backslashreplace'.
442 return output.decode('utf-8', 'replace')
443
444 # Run & package the results.
445 proc = subprocess.Popen(cmd, **kwargs)
446 (stdout, stderr) = proc.communicate(input=cmd_input)
447 dbg = ': ' + ' '.join(cmd)
448 if cmd_input is not None:
449 dbg += ' 0<|'
450 if stdout == subprocess.PIPE:
451 dbg += ' 1>|'
452 if stderr == subprocess.PIPE:
453 dbg += ' 2>|'
454 elif stderr == subprocess.STDOUT:
455 dbg += ' 2>&1'
456 trace.print(dbg)
457 ret = RunResult(proc.returncode, decode(stdout), decode(stderr))
458
459 # If things failed, print useful debugging output.
460 if check and ret.returncode:
461 print('repo: error: "%s" failed with exit status %s' %
462 (cmd[0], ret.returncode), file=sys.stderr)
463 print(' cwd: %s\n cmd: %r' %
464 (kwargs.get('cwd', os.getcwd()), cmd), file=sys.stderr)
465
466 def _print_output(name, output):
467 if output:
468 print(' %s:\n >> %s' % (name, '\n >> '.join(output.splitlines())),
469 file=sys.stderr)
470
471 _print_output('stdout', ret.stdout)
472 _print_output('stderr', ret.stderr)
473 raise RunError(ret)
474
475 return ret
476
264 477
265_gitc_manifest_dir = None 478_gitc_manifest_dir = None
266 479
@@ -283,9 +496,11 @@ def get_gitc_manifest_dir():
283def gitc_parse_clientdir(gitc_fs_path): 496def gitc_parse_clientdir(gitc_fs_path):
284 """Parse a path in the GITC FS and return its client name. 497 """Parse a path in the GITC FS and return its client name.
285 498
286 @param gitc_fs_path: A subdirectory path within the GITC_FS_ROOT_DIR. 499 Args:
500 gitc_fs_path: A subdirectory path within the GITC_FS_ROOT_DIR.
287 501
288 @returns: The GITC client name 502 Returns:
503 The GITC client name.
289 """ 504 """
290 if gitc_fs_path == GITC_FS_ROOT_DIR: 505 if gitc_fs_path == GITC_FS_ROOT_DIR:
291 return None 506 return None
@@ -309,31 +524,53 @@ class CloneFailure(Exception):
309 """ 524 """
310 525
311 526
527def check_repo_verify(repo_verify, quiet=False):
528 """Check the --repo-verify state."""
529 if not repo_verify:
530 print('repo: warning: verification of repo code has been disabled;\n'
531 'repo will not be able to verify the integrity of itself.\n',
532 file=sys.stderr)
533 return False
534
535 if NeedSetupGnuPG():
536 return SetupGnuPG(quiet)
537
538 return True
539
540
541def check_repo_rev(dst, rev, repo_verify=True, quiet=False):
542 """Check that |rev| is valid."""
543 do_verify = check_repo_verify(repo_verify, quiet=quiet)
544 remote_ref, local_rev = resolve_repo_rev(dst, rev)
545 if not quiet and not remote_ref.startswith('refs/heads/'):
546 print('warning: repo is not tracking a remote branch, so it will not '
547 'receive updates', file=sys.stderr)
548 if do_verify:
549 rev = verify_rev(dst, remote_ref, local_rev, quiet)
550 else:
551 rev = local_rev
552 return (remote_ref, rev)
553
554
312def _Init(args, gitc_init=False): 555def _Init(args, gitc_init=False):
313 """Installs repo by cloning it over the network. 556 """Installs repo by cloning it over the network.
314 """ 557 """
315 if gitc_init: 558 parser = GetParser(gitc_init=gitc_init)
316 _GitcInitOptions(init_optparse) 559 opt, args = parser.parse_args(args)
317 opt, args = init_optparse.parse_args(args)
318 if args: 560 if args:
319 init_optparse.print_usage() 561 if not opt.manifest_url:
320 sys.exit(1) 562 opt.manifest_url = args.pop(0)
321 563 if args:
322 url = opt.repo_url 564 parser.print_usage()
323 if not url: 565 sys.exit(1)
324 url = REPO_URL 566 opt.quiet = opt.output_mode is False
325 extra_args.append('--repo-url=%s' % url) 567 opt.verbose = opt.output_mode is True
326 568
327 branch = opt.repo_branch 569 if opt.clone_bundle is None:
328 if not branch: 570 opt.clone_bundle = False if opt.partial_clone else True
329 branch = REPO_REV
330 extra_args.append('--repo-branch=%s' % branch)
331 571
332 if branch.startswith('refs/heads/'): 572 url = opt.repo_url or REPO_URL
333 branch = branch[len('refs/heads/'):] 573 rev = opt.repo_rev or REPO_REV
334 if branch.startswith('refs/'):
335 print("fatal: invalid branch name '%s'" % branch, file=sys.stderr)
336 raise CloneFailure()
337 574
338 try: 575 try:
339 if gitc_init: 576 if gitc_init:
@@ -368,23 +605,13 @@ def _Init(args, gitc_init=False):
368 605
369 _CheckGitVersion() 606 _CheckGitVersion()
370 try: 607 try:
371 if opt.no_repo_verify: 608 if not opt.quiet:
372 do_verify = False 609 print('Downloading Repo source from', url)
373 else:
374 if NeedSetupGnuPG():
375 do_verify = SetupGnuPG(opt.quiet)
376 else:
377 do_verify = True
378
379 dst = os.path.abspath(os.path.join(repodir, S_repo)) 610 dst = os.path.abspath(os.path.join(repodir, S_repo))
380 _Clone(url, dst, opt.quiet, not opt.no_clone_bundle) 611 _Clone(url, dst, opt.clone_bundle, opt.quiet, opt.verbose)
381
382 if do_verify:
383 rev = _Verify(dst, branch, opt.quiet)
384 else:
385 rev = 'refs/remotes/origin/%s^0' % branch
386 612
387 _Checkout(dst, branch, rev, opt.quiet) 613 remote_ref, rev = check_repo_rev(dst, rev, opt.repo_verify, quiet=opt.quiet)
614 _Checkout(dst, remote_ref, rev, opt.quiet)
388 615
389 if not os.path.isfile(os.path.join(dst, 'repo')): 616 if not os.path.isfile(os.path.join(dst, 'repo')):
390 print("warning: '%s' does not look like a git-repo repository, is " 617 print("warning: '%s' does not look like a git-repo repository, is "
@@ -397,15 +624,34 @@ def _Init(args, gitc_init=False):
397 raise 624 raise
398 625
399 626
627def run_git(*args, **kwargs):
628 """Run git and return execution details."""
629 kwargs.setdefault('capture_output', True)
630 kwargs.setdefault('check', True)
631 try:
632 return run_command([GIT] + list(args), **kwargs)
633 except OSError as e:
634 print(file=sys.stderr)
635 print('repo: error: "%s" is not available' % GIT, file=sys.stderr)
636 print('repo: error: %s' % e, file=sys.stderr)
637 print(file=sys.stderr)
638 print('Please make sure %s is installed and in your path.' % GIT,
639 file=sys.stderr)
640 sys.exit(1)
641 except RunError:
642 raise CloneFailure()
643
644
400# The git version info broken down into components for easy analysis. 645# The git version info broken down into components for easy analysis.
401# Similar to Python's sys.version_info. 646# Similar to Python's sys.version_info.
402GitVersion = collections.namedtuple( 647GitVersion = collections.namedtuple(
403 'GitVersion', ('major', 'minor', 'micro', 'full')) 648 'GitVersion', ('major', 'minor', 'micro', 'full'))
404 649
650
405def ParseGitVersion(ver_str=None): 651def ParseGitVersion(ver_str=None):
406 if ver_str is None: 652 if ver_str is None:
407 # Load the version ourselves. 653 # Load the version ourselves.
408 ver_str = _GetGitVersion() 654 ver_str = run_git('--version').stdout
409 655
410 if not ver_str.startswith('git version '): 656 if not ver_str.startswith('git version '):
411 return None 657 return None
@@ -422,41 +668,52 @@ def ParseGitVersion(ver_str=None):
422 return GitVersion(*to_tuple) 668 return GitVersion(*to_tuple)
423 669
424 670
425def _GetGitVersion():
426 cmd = [GIT, '--version']
427 try:
428 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
429 except OSError as e:
430 print(file=sys.stderr)
431 print("fatal: '%s' is not available" % GIT, file=sys.stderr)
432 print('fatal: %s' % e, file=sys.stderr)
433 print(file=sys.stderr)
434 print('Please make sure %s is installed and in your path.' % GIT,
435 file=sys.stderr)
436 raise
437
438 ver_str = proc.stdout.read().strip()
439 proc.stdout.close()
440 proc.wait()
441 return ver_str.decode('utf-8')
442
443
444def _CheckGitVersion(): 671def _CheckGitVersion():
445 try: 672 ver_act = ParseGitVersion()
446 ver_act = ParseGitVersion()
447 except OSError:
448 raise CloneFailure()
449
450 if ver_act is None: 673 if ver_act is None:
451 print('fatal: unable to detect git version', file=sys.stderr) 674 print('fatal: unable to detect git version', file=sys.stderr)
452 raise CloneFailure() 675 raise CloneFailure()
453 676
454 if ver_act < MIN_GIT_VERSION: 677 if ver_act < MIN_GIT_VERSION:
455 need = '.'.join(map(str, MIN_GIT_VERSION)) 678 need = '.'.join(map(str, MIN_GIT_VERSION))
456 print('fatal: git %s or later required' % need, file=sys.stderr) 679 print('fatal: git %s or later required; found %s' % (need, ver_act.full),
680 file=sys.stderr)
457 raise CloneFailure() 681 raise CloneFailure()
458 682
459 683
684def SetGitTrace2ParentSid(env=None):
685 """Set up GIT_TRACE2_PARENT_SID for git tracing."""
686 # We roughly follow the format git itself uses in trace2/tr2_sid.c.
687 # (1) Be unique (2) be valid filename (3) be fixed length.
688 #
689 # Since we always export this variable, we try to avoid more expensive calls.
690 # e.g. We don't attempt hostname lookups or hashing the results.
691 if env is None:
692 env = os.environ
693
694 KEY = 'GIT_TRACE2_PARENT_SID'
695
696 now = datetime.datetime.utcnow()
697 value = 'repo-%s-P%08x' % (now.strftime('%Y%m%dT%H%M%SZ'), os.getpid())
698
699 # If it's already set, then append ourselves.
700 if KEY in env:
701 value = env[KEY] + '/' + value
702
703 _setenv(KEY, value, env=env)
704
705
706def _setenv(key, value, env=None):
707 """Set |key| in the OS environment |env| to |value|."""
708 if env is None:
709 env = os.environ
710 # Environment handling across systems is messy.
711 try:
712 env[key] = value
713 except UnicodeEncodeError:
714 env[key] = value.encode()
715
716
460def NeedSetupGnuPG(): 717def NeedSetupGnuPG():
461 if not os.path.isdir(home_dot_repo): 718 if not os.path.isdir(home_dot_repo):
462 return True 719 return True
@@ -492,43 +749,54 @@ def SetupGnuPG(quiet):
492 file=sys.stderr) 749 file=sys.stderr)
493 sys.exit(1) 750 sys.exit(1)
494 751
495 env = os.environ.copy() 752 if not quiet:
496 try: 753 print('repo: Updating release signing keys to keyset ver %s' %
497 env['GNUPGHOME'] = gpg_dir 754 ('.'.join(str(x) for x in KEYRING_VERSION),))
498 except UnicodeEncodeError: 755 # NB: We use --homedir (and cwd below) because some environments (Windows) do
499 env['GNUPGHOME'] = gpg_dir.encode() 756 # not correctly handle full native paths. We avoid the issue by changing to
500 757 # the right dir with cwd=gpg_dir before executing gpg, and then telling gpg to
501 cmd = ['gpg', '--import'] 758 # use the cwd (.) as its homedir which leaves the path resolution logic to it.
759 cmd = ['gpg', '--homedir', '.', '--import']
502 try: 760 try:
503 proc = subprocess.Popen(cmd, 761 # gpg can be pretty chatty. Always capture the output and if something goes
504 env=env, 762 # wrong, the builtin check failure will dump stdout & stderr for debugging.
505 stdin=subprocess.PIPE) 763 run_command(cmd, stdin=subprocess.PIPE, capture_output=True,
506 except OSError as e: 764 cwd=gpg_dir, check=True,
765 input=MAINTAINER_KEYS.encode('utf-8'))
766 except OSError:
507 if not quiet: 767 if not quiet:
508 print('warning: gpg (GnuPG) is not available.', file=sys.stderr) 768 print('warning: gpg (GnuPG) is not available.', file=sys.stderr)
509 print('warning: Installing it is strongly encouraged.', file=sys.stderr) 769 print('warning: Installing it is strongly encouraged.', file=sys.stderr)
510 print(file=sys.stderr) 770 print(file=sys.stderr)
511 return False 771 return False
512 772
513 proc.stdin.write(MAINTAINER_KEYS.encode('utf-8'))
514 proc.stdin.close()
515
516 if proc.wait() != 0:
517 print('fatal: registering repo maintainer keys failed', file=sys.stderr)
518 sys.exit(1)
519 print()
520
521 with open(os.path.join(home_dot_repo, 'keyring-version'), 'w') as fd: 773 with open(os.path.join(home_dot_repo, 'keyring-version'), 'w') as fd:
522 fd.write('.'.join(map(str, KEYRING_VERSION)) + '\n') 774 fd.write('.'.join(map(str, KEYRING_VERSION)) + '\n')
523 return True 775 return True
524 776
525 777
526def _SetConfig(local, name, value): 778def _SetConfig(cwd, name, value):
527 """Set a git configuration option to the specified value. 779 """Set a git configuration option to the specified value.
528 """ 780 """
529 cmd = [GIT, 'config', name, value] 781 run_git('config', name, value, cwd=cwd)
530 if subprocess.Popen(cmd, cwd=local).wait() != 0: 782
531 raise CloneFailure() 783
784def _GetRepoConfig(name):
785 """Read a repo configuration option."""
786 config = os.path.join(home_dot_repo, 'config')
787 if not os.path.exists(config):
788 return None
789
790 cmd = ['config', '--file', config, '--get', name]
791 ret = run_git(*cmd, check=False)
792 if ret.returncode == 0:
793 return ret.stdout
794 elif ret.returncode == 1:
795 return None
796 else:
797 print('repo: error: git %s failed:\n%s' % (' '.join(cmd), ret.stderr),
798 file=sys.stderr)
799 raise RunError()
532 800
533 801
534def _InitHttp(): 802def _InitHttp():
@@ -542,7 +810,7 @@ def _InitHttp():
542 p = n.hosts[host] 810 p = n.hosts[host]
543 mgr.add_password(p[1], 'http://%s/' % host, p[0], p[2]) 811 mgr.add_password(p[1], 'http://%s/' % host, p[0], p[2])
544 mgr.add_password(p[1], 'https://%s/' % host, p[0], p[2]) 812 mgr.add_password(p[1], 'https://%s/' % host, p[0], p[2])
545 except: 813 except Exception:
546 pass 814 pass
547 handlers.append(urllib.request.HTTPBasicAuthHandler(mgr)) 815 handlers.append(urllib.request.HTTPBasicAuthHandler(mgr))
548 handlers.append(urllib.request.HTTPDigestAuthHandler(mgr)) 816 handlers.append(urllib.request.HTTPDigestAuthHandler(mgr))
@@ -556,39 +824,29 @@ def _InitHttp():
556 urllib.request.install_opener(urllib.request.build_opener(*handlers)) 824 urllib.request.install_opener(urllib.request.build_opener(*handlers))
557 825
558 826
559def _Fetch(url, local, src, quiet): 827def _Fetch(url, cwd, src, quiet, verbose):
560 if not quiet: 828 cmd = ['fetch']
561 print('Get %s' % url, file=sys.stderr) 829 if not verbose:
562
563 cmd = [GIT, 'fetch']
564 if quiet:
565 cmd.append('--quiet') 830 cmd.append('--quiet')
831 err = None
832 if not quiet and sys.stdout.isatty():
833 cmd.append('--progress')
834 elif not verbose:
566 err = subprocess.PIPE 835 err = subprocess.PIPE
567 else:
568 err = None
569 cmd.append(src) 836 cmd.append(src)
570 cmd.append('+refs/heads/*:refs/remotes/origin/*') 837 cmd.append('+refs/heads/*:refs/remotes/origin/*')
571 cmd.append('+refs/tags/*:refs/tags/*') 838 cmd.append('+refs/tags/*:refs/tags/*')
572 839 run_git(*cmd, stderr=err, capture_output=False, cwd=cwd)
573 proc = subprocess.Popen(cmd, cwd=local, stderr=err)
574 if err:
575 proc.stderr.read()
576 proc.stderr.close()
577 if proc.wait() != 0:
578 raise CloneFailure()
579 840
580 841
581def _DownloadBundle(url, local, quiet): 842def _DownloadBundle(url, cwd, quiet, verbose):
582 if not url.endswith('/'): 843 if not url.endswith('/'):
583 url += '/' 844 url += '/'
584 url += 'clone.bundle' 845 url += 'clone.bundle'
585 846
586 proc = subprocess.Popen( 847 ret = run_git('config', '--get-regexp', 'url.*.insteadof', cwd=cwd,
587 [GIT, 'config', '--get-regexp', 'url.*.insteadof'], 848 check=False)
588 cwd=local, 849 for line in ret.stdout.splitlines():
589 stdout=subprocess.PIPE)
590 for line in proc.stdout:
591 line = line.decode('utf-8')
592 m = re.compile(r'^url\.(.*)\.insteadof (.*)$').match(line) 850 m = re.compile(r'^url\.(.*)\.insteadof (.*)$').match(line)
593 if m: 851 if m:
594 new_url = m.group(1) 852 new_url = m.group(1)
@@ -596,29 +854,26 @@ def _DownloadBundle(url, local, quiet):
596 if url.startswith(old_url): 854 if url.startswith(old_url):
597 url = new_url + url[len(old_url):] 855 url = new_url + url[len(old_url):]
598 break 856 break
599 proc.stdout.close()
600 proc.wait()
601 857
602 if not url.startswith('http:') and not url.startswith('https:'): 858 if not url.startswith('http:') and not url.startswith('https:'):
603 return False 859 return False
604 860
605 dest = open(os.path.join(local, '.git', 'clone.bundle'), 'w+b') 861 dest = open(os.path.join(cwd, '.git', 'clone.bundle'), 'w+b')
606 try: 862 try:
607 try: 863 try:
608 r = urllib.request.urlopen(url) 864 r = urllib.request.urlopen(url)
609 except urllib.error.HTTPError as e: 865 except urllib.error.HTTPError as e:
610 if e.code in [401, 403, 404, 501]: 866 if e.code not in [400, 401, 403, 404, 501]:
611 return False 867 print('warning: Cannot get %s' % url, file=sys.stderr)
612 print('fatal: Cannot get %s' % url, file=sys.stderr) 868 print('warning: HTTP error %s' % e.code, file=sys.stderr)
613 print('fatal: HTTP error %s' % e.code, file=sys.stderr) 869 return False
614 raise CloneFailure()
615 except urllib.error.URLError as e: 870 except urllib.error.URLError as e:
616 print('fatal: Cannot get %s' % url, file=sys.stderr) 871 print('fatal: Cannot get %s' % url, file=sys.stderr)
617 print('fatal: error %s' % e.reason, file=sys.stderr) 872 print('fatal: error %s' % e.reason, file=sys.stderr)
618 raise CloneFailure() 873 raise CloneFailure()
619 try: 874 try:
620 if not quiet: 875 if verbose:
621 print('Get %s' % url, file=sys.stderr) 876 print('Downloading clone bundle %s' % url, file=sys.stderr)
622 while True: 877 while True:
623 buf = r.read(8192) 878 buf = r.read(8192)
624 if not buf: 879 if not buf:
@@ -630,124 +885,139 @@ def _DownloadBundle(url, local, quiet):
630 dest.close() 885 dest.close()
631 886
632 887
633def _ImportBundle(local): 888def _ImportBundle(cwd):
634 path = os.path.join(local, '.git', 'clone.bundle') 889 path = os.path.join(cwd, '.git', 'clone.bundle')
635 try: 890 try:
636 _Fetch(local, local, path, True) 891 _Fetch(cwd, cwd, path, True, False)
637 finally: 892 finally:
638 os.remove(path) 893 os.remove(path)
639 894
640 895
641def _Clone(url, local, quiet, clone_bundle): 896def _Clone(url, cwd, clone_bundle, quiet, verbose):
642 """Clones a git repository to a new subdirectory of repodir 897 """Clones a git repository to a new subdirectory of repodir
643 """ 898 """
644 try: 899 if verbose:
645 os.mkdir(local) 900 print('Cloning git repository', url)
646 except OSError as e:
647 print('fatal: cannot make %s directory: %s' % (local, e.strerror),
648 file=sys.stderr)
649 raise CloneFailure()
650 901
651 cmd = [GIT, 'init', '--quiet']
652 try: 902 try:
653 proc = subprocess.Popen(cmd, cwd=local) 903 os.mkdir(cwd)
654 except OSError as e: 904 except OSError as e:
655 print(file=sys.stderr) 905 print('fatal: cannot make %s directory: %s' % (cwd, e.strerror),
656 print("fatal: '%s' is not available" % GIT, file=sys.stderr)
657 print('fatal: %s' % e, file=sys.stderr)
658 print(file=sys.stderr)
659 print('Please make sure %s is installed and in your path.' % GIT,
660 file=sys.stderr) 906 file=sys.stderr)
661 raise CloneFailure() 907 raise CloneFailure()
662 if proc.wait() != 0: 908
663 print('fatal: could not create %s' % local, file=sys.stderr) 909 run_git('init', '--quiet', cwd=cwd)
664 raise CloneFailure()
665 910
666 _InitHttp() 911 _InitHttp()
667 _SetConfig(local, 'remote.origin.url', url) 912 _SetConfig(cwd, 'remote.origin.url', url)
668 _SetConfig(local, 913 _SetConfig(cwd,
669 'remote.origin.fetch', 914 'remote.origin.fetch',
670 '+refs/heads/*:refs/remotes/origin/*') 915 '+refs/heads/*:refs/remotes/origin/*')
671 if clone_bundle and _DownloadBundle(url, local, quiet): 916 if clone_bundle and _DownloadBundle(url, cwd, quiet, verbose):
672 _ImportBundle(local) 917 _ImportBundle(cwd)
673 _Fetch(url, local, 'origin', quiet) 918 _Fetch(url, cwd, 'origin', quiet, verbose)
919
920
921def resolve_repo_rev(cwd, committish):
922 """Figure out what REPO_REV represents.
674 923
924 We support:
925 * refs/heads/xxx: Branch.
926 * refs/tags/xxx: Tag.
927 * xxx: Branch or tag or commit.
675 928
676def _Verify(cwd, branch, quiet): 929 Args:
677 """Verify the branch has been signed by a tag. 930 cwd: The git checkout to run in.
931 committish: The REPO_REV argument to resolve.
932
933 Returns:
934 A tuple of (remote ref, commit) as makes sense for the committish.
935 For branches, this will look like ('refs/heads/stable', <revision>).
936 For tags, this will look like ('refs/tags/v1.0', <revision>).
937 For commits, this will be (<revision>, <revision>).
678 """ 938 """
679 cmd = [GIT, 'describe', 'origin/%s' % branch] 939 def resolve(committish):
680 proc = subprocess.Popen(cmd, 940 ret = run_git('rev-parse', '--verify', '%s^{commit}' % (committish,),
681 stdout=subprocess.PIPE, 941 cwd=cwd, check=False)
682 stderr=subprocess.PIPE, 942 return None if ret.returncode else ret.stdout.strip()
683 cwd=cwd) 943
684 cur = proc.stdout.read().strip().decode('utf-8') 944 # An explicit branch.
685 proc.stdout.close() 945 if committish.startswith('refs/heads/'):
686 946 remote_ref = committish
687 proc.stderr.read() 947 committish = committish[len('refs/heads/'):]
688 proc.stderr.close() 948 rev = resolve('refs/remotes/origin/%s' % committish)
689 949 if rev is None:
690 if proc.wait() != 0 or not cur: 950 print('repo: error: unknown branch "%s"' % (committish,),
691 print(file=sys.stderr) 951 file=sys.stderr)
692 print("fatal: branch '%s' has not been signed" % branch, file=sys.stderr) 952 raise CloneFailure()
693 raise CloneFailure() 953 return (remote_ref, rev)
954
955 # An explicit tag.
956 if committish.startswith('refs/tags/'):
957 remote_ref = committish
958 committish = committish[len('refs/tags/'):]
959 rev = resolve(remote_ref)
960 if rev is None:
961 print('repo: error: unknown tag "%s"' % (committish,),
962 file=sys.stderr)
963 raise CloneFailure()
964 return (remote_ref, rev)
965
966 # See if it's a short branch name.
967 rev = resolve('refs/remotes/origin/%s' % committish)
968 if rev:
969 return ('refs/heads/%s' % (committish,), rev)
970
971 # See if it's a tag.
972 rev = resolve('refs/tags/%s' % committish)
973 if rev:
974 return ('refs/tags/%s' % (committish,), rev)
975
976 # See if it's a commit.
977 rev = resolve(committish)
978 if rev and rev.lower().startswith(committish.lower()):
979 return (rev, rev)
980
981 # Give up!
982 print('repo: error: unable to resolve "%s"' % (committish,), file=sys.stderr)
983 raise CloneFailure()
984
985
986def verify_rev(cwd, remote_ref, rev, quiet):
987 """Verify the commit has been signed by a tag."""
988 ret = run_git('describe', rev, cwd=cwd)
989 cur = ret.stdout.strip()
694 990
695 m = re.compile(r'^(.*)-[0-9]{1,}-g[0-9a-f]{1,}$').match(cur) 991 m = re.compile(r'^(.*)-[0-9]{1,}-g[0-9a-f]{1,}$').match(cur)
696 if m: 992 if m:
697 cur = m.group(1) 993 cur = m.group(1)
698 if not quiet: 994 if not quiet:
699 print(file=sys.stderr) 995 print(file=sys.stderr)
700 print("info: Ignoring branch '%s'; using tagged release '%s'" 996 print("warning: '%s' is not signed; falling back to signed release '%s'"
701 % (branch, cur), file=sys.stderr) 997 % (remote_ref, cur), file=sys.stderr)
702 print(file=sys.stderr) 998 print(file=sys.stderr)
703 999
704 env = os.environ.copy() 1000 env = os.environ.copy()
705 try: 1001 _setenv('GNUPGHOME', gpg_dir, env)
706 env['GNUPGHOME'] = gpg_dir 1002 run_git('tag', '-v', cur, cwd=cwd, env=env)
707 except UnicodeEncodeError:
708 env['GNUPGHOME'] = gpg_dir.encode()
709
710 cmd = [GIT, 'tag', '-v', cur]
711 proc = subprocess.Popen(cmd,
712 stdout=subprocess.PIPE,
713 stderr=subprocess.PIPE,
714 cwd=cwd,
715 env=env)
716 out = proc.stdout.read().decode('utf-8')
717 proc.stdout.close()
718
719 err = proc.stderr.read().decode('utf-8')
720 proc.stderr.close()
721
722 if proc.wait() != 0:
723 print(file=sys.stderr)
724 print(out, file=sys.stderr)
725 print(err, file=sys.stderr)
726 print(file=sys.stderr)
727 raise CloneFailure()
728 return '%s^0' % cur 1003 return '%s^0' % cur
729 1004
730 1005
731def _Checkout(cwd, branch, rev, quiet): 1006def _Checkout(cwd, remote_ref, rev, quiet):
732 """Checkout an upstream branch into the repository and track it. 1007 """Checkout an upstream branch into the repository and track it.
733 """ 1008 """
734 cmd = [GIT, 'update-ref', 'refs/heads/default', rev] 1009 run_git('update-ref', 'refs/heads/default', rev, cwd=cwd)
735 if subprocess.Popen(cmd, cwd=cwd).wait() != 0:
736 raise CloneFailure()
737 1010
738 _SetConfig(cwd, 'branch.default.remote', 'origin') 1011 _SetConfig(cwd, 'branch.default.remote', 'origin')
739 _SetConfig(cwd, 'branch.default.merge', 'refs/heads/%s' % branch) 1012 _SetConfig(cwd, 'branch.default.merge', remote_ref)
740 1013
741 cmd = [GIT, 'symbolic-ref', 'HEAD', 'refs/heads/default'] 1014 run_git('symbolic-ref', 'HEAD', 'refs/heads/default', cwd=cwd)
742 if subprocess.Popen(cmd, cwd=cwd).wait() != 0:
743 raise CloneFailure()
744 1015
745 cmd = [GIT, 'read-tree', '--reset', '-u'] 1016 cmd = ['read-tree', '--reset', '-u']
746 if not quiet: 1017 if not quiet:
747 cmd.append('-v') 1018 cmd.append('-v')
748 cmd.append('HEAD') 1019 cmd.append('HEAD')
749 if subprocess.Popen(cmd, cwd=cwd).wait() != 0: 1020 run_git(*cmd, cwd=cwd)
750 raise CloneFailure()
751 1021
752 1022
753def _FindRepo(): 1023def _FindRepo():
@@ -757,9 +1027,7 @@ def _FindRepo():
757 repo = None 1027 repo = None
758 1028
759 olddir = None 1029 olddir = None
760 while curdir != '/' \ 1030 while curdir != olddir and not repo:
761 and curdir != olddir \
762 and not repo:
763 repo = os.path.join(curdir, repodir, REPO_MAIN) 1031 repo = os.path.join(curdir, repodir, REPO_MAIN)
764 if not os.path.isfile(repo): 1032 if not os.path.isfile(repo):
765 repo = None 1033 repo = None
@@ -770,6 +1038,26 @@ def _FindRepo():
770 1038
771class _Options(object): 1039class _Options(object):
772 help = False 1040 help = False
1041 version = False
1042
1043
1044def _ExpandAlias(name):
1045 """Look up user registered aliases."""
1046 # We don't resolve aliases for existing subcommands. This matches git.
1047 if name in {'gitc-init', 'help', 'init'}:
1048 return name, []
1049
1050 alias = _GetRepoConfig('alias.%s' % (name,))
1051 if alias is None:
1052 return name, []
1053
1054 args = alias.strip().split(' ', 1)
1055 name = args[0]
1056 if len(args) == 2:
1057 args = shlex.split(args[1])
1058 else:
1059 args = []
1060 return name, args
773 1061
774 1062
775def _ParseArguments(args): 1063def _ParseArguments(args):
@@ -781,7 +1069,10 @@ def _ParseArguments(args):
781 a = args[i] 1069 a = args[i]
782 if a == '-h' or a == '--help': 1070 if a == '-h' or a == '--help':
783 opt.help = True 1071 opt.help = True
784 1072 elif a == '--version':
1073 opt.version = True
1074 elif a == '--trace':
1075 trace.set(True)
785 elif not a.startswith('-'): 1076 elif not a.startswith('-'):
786 cmd = a 1077 cmd = a
787 arg = args[i + 1:] 1078 arg = args[i + 1:]
@@ -789,6 +1080,90 @@ def _ParseArguments(args):
789 return cmd, opt, arg 1080 return cmd, opt, arg
790 1081
791 1082
1083class Requirements(object):
1084 """Helper for checking repo's system requirements."""
1085
1086 REQUIREMENTS_NAME = 'requirements.json'
1087
1088 def __init__(self, requirements):
1089 """Initialize.
1090
1091 Args:
1092 requirements: A dictionary of settings.
1093 """
1094 self.requirements = requirements
1095
1096 @classmethod
1097 def from_dir(cls, path):
1098 return cls.from_file(os.path.join(path, cls.REQUIREMENTS_NAME))
1099
1100 @classmethod
1101 def from_file(cls, path):
1102 try:
1103 with open(path, 'rb') as f:
1104 data = f.read()
1105 except EnvironmentError:
1106 # NB: EnvironmentError is used for Python 2 & 3 compatibility.
1107 # If we couldn't open the file, assume it's an old source tree.
1108 return None
1109
1110 return cls.from_data(data)
1111
1112 @classmethod
1113 def from_data(cls, data):
1114 comment_line = re.compile(br'^ *#')
1115 strip_data = b''.join(x for x in data.splitlines() if not comment_line.match(x))
1116 try:
1117 json_data = json.loads(strip_data)
1118 except Exception: # pylint: disable=broad-except
1119 # If we couldn't parse it, assume it's incompatible.
1120 return None
1121
1122 return cls(json_data)
1123
1124 def _get_soft_ver(self, pkg):
1125 """Return the soft version for |pkg| if it exists."""
1126 return self.requirements.get(pkg, {}).get('soft', ())
1127
1128 def _get_hard_ver(self, pkg):
1129 """Return the hard version for |pkg| if it exists."""
1130 return self.requirements.get(pkg, {}).get('hard', ())
1131
1132 @staticmethod
1133 def _format_ver(ver):
1134 """Return a dotted version from |ver|."""
1135 return '.'.join(str(x) for x in ver)
1136
1137 def assert_ver(self, pkg, curr_ver):
1138 """Verify |pkg|'s |curr_ver| is new enough."""
1139 curr_ver = tuple(curr_ver)
1140 soft_ver = tuple(self._get_soft_ver(pkg))
1141 hard_ver = tuple(self._get_hard_ver(pkg))
1142 if curr_ver < hard_ver:
1143 print('repo: error: Your version of "%s" (%s) is unsupported; '
1144 'Please upgrade to at least version %s to continue.' %
1145 (pkg, self._format_ver(curr_ver), self._format_ver(soft_ver)),
1146 file=sys.stderr)
1147 sys.exit(1)
1148
1149 if curr_ver < soft_ver:
1150 print('repo: warning: Your version of "%s" (%s) is no longer supported; '
1151 'Please upgrade to at least version %s to avoid breakage.' %
1152 (pkg, self._format_ver(curr_ver), self._format_ver(soft_ver)),
1153 file=sys.stderr)
1154
1155 def assert_all(self):
1156 """Assert all of the requirements are satisified."""
1157 # See if we need a repo launcher upgrade first.
1158 self.assert_ver('repo', VERSION)
1159
1160 # Check python before we try to import the repo code.
1161 self.assert_ver('python', sys.version_info)
1162
1163 # Check git while we're at it.
1164 self.assert_ver('git', ParseGitVersion())
1165
1166
792def _Usage(): 1167def _Usage():
793 gitc_usage = "" 1168 gitc_usage = ""
794 if get_gitc_manifest_dir(): 1169 if get_gitc_manifest_dir():
@@ -807,17 +1182,15 @@ The most commonly used repo commands are:
807 1182
808For access to the full online help, install repo ("repo init"). 1183For access to the full online help, install repo ("repo init").
809""") 1184""")
1185 print('Bug reports:', BUG_URL)
810 sys.exit(0) 1186 sys.exit(0)
811 1187
812 1188
813def _Help(args): 1189def _Help(args):
814 if args: 1190 if args:
815 if args[0] == 'init': 1191 if args[0] in {'init', 'gitc-init'}:
816 init_optparse.print_help() 1192 parser = GetParser(gitc_init=args[0] == 'gitc-init')
817 sys.exit(0) 1193 parser.print_help()
818 elif args[0] == 'gitc-init':
819 _GitcInitOptions(init_optparse)
820 init_optparse.print_help()
821 sys.exit(0) 1194 sys.exit(0)
822 else: 1195 else:
823 print("error: '%s' is not a bootstrap command.\n" 1196 print("error: '%s' is not a bootstrap command.\n"
@@ -828,6 +1201,25 @@ def _Help(args):
828 sys.exit(1) 1201 sys.exit(1)
829 1202
830 1203
1204def _Version():
1205 """Show version information."""
1206 print('<repo not installed>')
1207 print('repo launcher version %s' % ('.'.join(str(x) for x in VERSION),))
1208 print(' (from %s)' % (__file__,))
1209 print('git %s' % (ParseGitVersion().full,))
1210 print('Python %s' % sys.version)
1211 uname = platform.uname()
1212 if sys.version_info.major < 3:
1213 # Python 3 returns a named tuple, but Python 2 is simpler.
1214 print(uname)
1215 else:
1216 print('OS %s %s (%s)' % (uname.system, uname.release, uname.version))
1217 print('CPU %s (%s)' %
1218 (uname.machine, uname.processor if uname.processor else 'unknown'))
1219 print('Bug reports:', BUG_URL)
1220 sys.exit(0)
1221
1222
831def _NotInstalled(): 1223def _NotInstalled():
832 print('error: repo is not installed. Use "repo init" to install it here.', 1224 print('error: repo is not installed. Use "repo init" to install it here.',
833 file=sys.stderr) 1225 file=sys.stderr)
@@ -860,26 +1252,26 @@ def _SetDefaultsTo(gitdir):
860 global REPO_REV 1252 global REPO_REV
861 1253
862 REPO_URL = gitdir 1254 REPO_URL = gitdir
863 proc = subprocess.Popen([GIT, 1255 ret = run_git('--git-dir=%s' % gitdir, 'symbolic-ref', 'HEAD', check=False)
864 '--git-dir=%s' % gitdir, 1256 if ret.returncode:
865 'symbolic-ref', 1257 # If we're not tracking a branch (bisect/etc...), then fall back to commit.
866 'HEAD'], 1258 print('repo: warning: %s has no current branch; using HEAD' % gitdir,
867 stdout=subprocess.PIPE, 1259 file=sys.stderr)
868 stderr=subprocess.PIPE) 1260 try:
869 REPO_REV = proc.stdout.read().strip().decode('utf-8') 1261 ret = run_git('rev-parse', 'HEAD', cwd=gitdir)
870 proc.stdout.close() 1262 except CloneFailure:
871 1263 print('fatal: %s has invalid HEAD' % gitdir, file=sys.stderr)
872 proc.stderr.read() 1264 sys.exit(1)
873 proc.stderr.close() 1265
874 1266 REPO_REV = ret.stdout.strip()
875 if proc.wait() != 0:
876 print('fatal: %s has no current branch' % gitdir, file=sys.stderr)
877 sys.exit(1)
878 1267
879 1268
880def main(orig_args): 1269def main(orig_args):
881 cmd, opt, args = _ParseArguments(orig_args) 1270 cmd, opt, args = _ParseArguments(orig_args)
882 1271
1272 # We run this early as we run some git commands ourselves.
1273 SetGitTrace2ParentSid()
1274
883 repo_main, rel_repo_dir = None, None 1275 repo_main, rel_repo_dir = None, None
884 # Don't use the local repo copy, make sure to switch to the gitc client first. 1276 # Don't use the local repo copy, make sure to switch to the gitc client first.
885 if cmd != 'gitc-init': 1277 if cmd != 'gitc-init':
@@ -896,10 +1288,17 @@ def main(orig_args):
896 file=sys.stderr) 1288 file=sys.stderr)
897 sys.exit(1) 1289 sys.exit(1)
898 if not repo_main: 1290 if not repo_main:
1291 # Only expand aliases here since we'll be parsing the CLI ourselves.
1292 # If we had repo_main, alias expansion would happen in main.py.
1293 cmd, alias_args = _ExpandAlias(cmd)
1294 args = alias_args + args
1295
899 if opt.help: 1296 if opt.help:
900 _Usage() 1297 _Usage()
901 if cmd == 'help': 1298 if cmd == 'help':
902 _Help(args) 1299 _Help(args)
1300 if opt.version or cmd == 'version':
1301 _Version()
903 if not cmd: 1302 if not cmd:
904 _NotInstalled() 1303 _NotInstalled()
905 if cmd == 'init' or cmd == 'gitc-init': 1304 if cmd == 'init' or cmd == 'gitc-init':
@@ -920,6 +1319,14 @@ def main(orig_args):
920 if my_main: 1319 if my_main:
921 repo_main = my_main 1320 repo_main = my_main
922 1321
1322 if not repo_main:
1323 print("fatal: unable to find repo entry point", file=sys.stderr)
1324 sys.exit(1)
1325
1326 reqs = Requirements.from_dir(os.path.dirname(repo_main))
1327 if reqs:
1328 reqs.assert_all()
1329
923 ver_str = '.'.join(map(str, VERSION)) 1330 ver_str = '.'.join(map(str, VERSION))
924 me = [sys.executable, repo_main, 1331 me = [sys.executable, repo_main,
925 '--repo-dir=%s' % rel_repo_dir, 1332 '--repo-dir=%s' % rel_repo_dir,
@@ -927,16 +1334,9 @@ def main(orig_args):
927 '--wrapper-path=%s' % wrapper_path, 1334 '--wrapper-path=%s' % wrapper_path,
928 '--'] 1335 '--']
929 me.extend(orig_args) 1336 me.extend(orig_args)
930 me.extend(extra_args) 1337 exec_command(me)
931 try: 1338 print("fatal: unable to start %s" % repo_main, file=sys.stderr)
932 if platform.system() == "Windows": 1339 sys.exit(148)
933 sys.exit(subprocess.call(me))
934 else:
935 os.execv(sys.executable, me)
936 except OSError as e:
937 print("fatal: unable to start %s" % repo_main, file=sys.stderr)
938 print("fatal: %s" % e, file=sys.stderr)
939 sys.exit(148)
940 1340
941 1341
942if __name__ == '__main__': 1342if __name__ == '__main__':
diff --git a/repo_trace.py b/repo_trace.py
index f5bc76d4..7be0c045 100644
--- a/repo_trace.py
+++ b/repo_trace.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -19,7 +17,6 @@
19Activated via `repo --trace ...` or `REPO_TRACE=1 repo ...`. 17Activated via `repo --trace ...` or `REPO_TRACE=1 repo ...`.
20""" 18"""
21 19
22from __future__ import print_function
23import sys 20import sys
24import os 21import os
25 22
@@ -28,13 +25,16 @@ REPO_TRACE = 'REPO_TRACE'
28 25
29_TRACE = os.environ.get(REPO_TRACE) == '1' 26_TRACE = os.environ.get(REPO_TRACE) == '1'
30 27
28
31def IsTrace(): 29def IsTrace():
32 return _TRACE 30 return _TRACE
33 31
32
34def SetTrace(): 33def SetTrace():
35 global _TRACE 34 global _TRACE
36 _TRACE = True 35 _TRACE = True
37 36
37
38def Trace(fmt, *args): 38def Trace(fmt, *args):
39 if IsTrace(): 39 if IsTrace():
40 print(fmt % args, file=sys.stderr) 40 print(fmt % args, file=sys.stderr)
diff --git a/requirements.json b/requirements.json
new file mode 100644
index 00000000..cb55cd25
--- /dev/null
+++ b/requirements.json
@@ -0,0 +1,57 @@
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.7.2 is in Debian Squeeze.
50 # git-1.7.9 is in Ubuntu Precise.
51 # git-1.9.1 is in Ubuntu Trusty.
52 # git-1.7.10 is in Debian Wheezy.
53 "git": {
54 "hard": [1, 7, 2],
55 "soft": [1, 9, 1]
56 }
57}
diff --git a/run_tests b/run_tests
index d7144b3c..573dd446 100755
--- a/run_tests
+++ b/run_tests
@@ -1,5 +1,4 @@
1#!/usr/bin/env python 1#!/usr/bin/env python3
2# -*- coding:utf-8 -*-
3# Copyright 2019 The Android Open Source Project 2# Copyright 2019 The Android Open Source Project
4# 3#
5# Licensed under the Apache License, Version 2.0 (the "License"); 4# Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,37 +15,46 @@
16 15
17"""Wrapper to run pytest with the right settings.""" 16"""Wrapper to run pytest with the right settings."""
18 17
19from __future__ import print_function
20
21import errno 18import errno
22import os 19import os
20import shutil
23import subprocess 21import subprocess
24import sys 22import sys
25 23
26 24
27def run_pytest(cmd, argv): 25def find_pytest():
28 """Run the unittests via |cmd|.""" 26 """Try to locate a good version of pytest."""
29 try: 27 # If we're in a virtualenv, assume that it's provided the right pytest.
30 return subprocess.call([cmd] + argv) 28 if 'VIRTUAL_ENV' in os.environ:
31 except OSError as e: 29 return 'pytest'
32 if e.errno == errno.ENOENT: 30
33 print('%s: unable to run `%s`: %s' % (__file__, cmd, e), file=sys.stderr) 31 # Use the Python 3 version if available.
34 print('%s: Try installing pytest: sudo apt-get install python-pytest' % 32 ret = shutil.which('pytest-3')
35 (__file__,), file=sys.stderr) 33 if ret:
36 return 127 34 return ret
37 else: 35
38 raise 36 # Hopefully this is a Python 3 version.
37 ret = shutil.which('pytest')
38 if ret:
39 return ret
40
41 print('%s: unable to find pytest.' % (__file__,), file=sys.stderr)
42 print('%s: Try installing: sudo apt-get install python-pytest' % (__file__,),
43 file=sys.stderr)
39 44
40 45
41def main(argv): 46def main(argv):
42 """The main entry.""" 47 """The main entry."""
43 # Add the repo tree to PYTHONPATH as the tests expect to be able to import 48 # Add the repo tree to PYTHONPATH as the tests expect to be able to import
44 # modules directly. 49 # modules directly.
45 topdir = os.path.dirname(os.path.realpath(__file__)) 50 pythonpath = os.path.dirname(os.path.realpath(__file__))
46 pythonpath = os.environ.get('PYTHONPATH', '') 51 oldpythonpath = os.environ.get('PYTHONPATH', None)
47 os.environ['PYTHONPATH'] = '%s:%s' % (topdir, pythonpath) 52 if oldpythonpath is not None:
48 53 pythonpath += os.pathsep + oldpythonpath
49 return run_pytest('pytest', argv) 54 os.environ['PYTHONPATH'] = pythonpath
55
56 pytest = find_pytest()
57 return subprocess.run([pytest] + argv, check=False).returncode
50 58
51 59
52if __name__ == '__main__': 60if __name__ == '__main__':
diff --git a/setup.py b/setup.py
index f4d7728d..17aeae22 100755
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,4 @@
1#!/usr/bin/env python 1#!/usr/bin/env python3
2# -*- coding:utf-8 -*-
3# Copyright 2019 The Android Open Source Project 2# Copyright 2019 The Android Open Source Project
4# 3#
5# Licensed under the Apache License, Version 2.0 (the 'License"); 4# Licensed under the Apache License, Version 2.0 (the 'License");
@@ -16,8 +15,6 @@
16 15
17"""Python packaging for repo.""" 16"""Python packaging for repo."""
18 17
19from __future__ import print_function
20
21import os 18import os
22import setuptools 19import setuptools
23 20
@@ -35,7 +32,7 @@ with open(os.path.join(TOPDIR, 'README.md')) as fp:
35# https://packaging.python.org/tutorials/packaging-projects/ 32# https://packaging.python.org/tutorials/packaging-projects/
36setuptools.setup( 33setuptools.setup(
37 name='repo', 34 name='repo',
38 version='1.13.8', 35 version='2',
39 maintainer='Various', 36 maintainer='Various',
40 maintainer_email='repo-discuss@googlegroups.com', 37 maintainer_email='repo-discuss@googlegroups.com',
41 description='Repo helps manage many Git repositories', 38 description='Repo helps manage many Git repositories',
@@ -55,9 +52,10 @@ setuptools.setup(
55 'Operating System :: MacOS :: MacOS X', 52 'Operating System :: MacOS :: MacOS X',
56 'Operating System :: Microsoft :: Windows :: Windows 10', 53 'Operating System :: Microsoft :: Windows :: Windows 10',
57 'Operating System :: POSIX :: Linux', 54 'Operating System :: POSIX :: Linux',
55 'Programming Language :: Python :: 3',
56 'Programming Language :: Python :: 3 :: Only',
58 'Topic :: Software Development :: Version Control :: Git', 57 'Topic :: Software Development :: Version Control :: Git',
59 ], 58 ],
60 # We support Python 2.7 and Python 3.6+. 59 python_requires='>=3.6',
61 python_requires='>=2.7, ' + ', '.join('!=3.%i.*' % x for x in range(0, 6)),
62 packages=['subcmds'], 60 packages=['subcmds'],
63) 61)
diff --git a/ssh.py b/ssh.py
new file mode 100644
index 00000000..0ae8d120
--- /dev/null
+++ b/ssh.py
@@ -0,0 +1,277 @@
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"""Common SSH management logic."""
16
17import functools
18import multiprocessing
19import os
20import re
21import signal
22import subprocess
23import sys
24import tempfile
25import time
26
27import platform_utils
28from repo_trace import Trace
29
30
31PROXY_PATH = os.path.join(os.path.dirname(__file__), 'git_ssh')
32
33
34def _run_ssh_version():
35 """run ssh -V to display the version number"""
36 return subprocess.check_output(['ssh', '-V'], stderr=subprocess.STDOUT).decode()
37
38
39def _parse_ssh_version(ver_str=None):
40 """parse a ssh version string into a tuple"""
41 if ver_str is None:
42 ver_str = _run_ssh_version()
43 m = re.match(r'^OpenSSH_([0-9.]+)(p[0-9]+)?\s', ver_str)
44 if m:
45 return tuple(int(x) for x in m.group(1).split('.'))
46 else:
47 return ()
48
49
50@functools.lru_cache(maxsize=None)
51def version():
52 """return ssh version as a tuple"""
53 try:
54 return _parse_ssh_version()
55 except subprocess.CalledProcessError:
56 print('fatal: unable to detect ssh version', file=sys.stderr)
57 sys.exit(1)
58
59
60URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):')
61URI_ALL = re.compile(r'^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/')
62
63
64class ProxyManager:
65 """Manage various ssh clients & masters that we spawn.
66
67 This will take care of sharing state between multiprocessing children, and
68 make sure that if we crash, we don't leak any of the ssh sessions.
69
70 The code should work with a single-process scenario too, and not add too much
71 overhead due to the manager.
72 """
73
74 # Path to the ssh program to run which will pass our master settings along.
75 # Set here more as a convenience API.
76 proxy = PROXY_PATH
77
78 def __init__(self, manager):
79 # Protect access to the list of active masters.
80 self._lock = multiprocessing.Lock()
81 # List of active masters (pid). These will be spawned on demand, and we are
82 # responsible for shutting them all down at the end.
83 self._masters = manager.list()
84 # Set of active masters indexed by "host:port" information.
85 # The value isn't used, but multiprocessing doesn't provide a set class.
86 self._master_keys = manager.dict()
87 # Whether ssh masters are known to be broken, so we give up entirely.
88 self._master_broken = manager.Value('b', False)
89 # List of active ssh sesssions. Clients will be added & removed as
90 # connections finish, so this list is just for safety & cleanup if we crash.
91 self._clients = manager.list()
92 # Path to directory for holding master sockets.
93 self._sock_path = None
94
95 def __enter__(self):
96 """Enter a new context."""
97 return self
98
99 def __exit__(self, exc_type, exc_value, traceback):
100 """Exit a context & clean up all resources."""
101 self.close()
102
103 def add_client(self, proc):
104 """Track a new ssh session."""
105 self._clients.append(proc.pid)
106
107 def remove_client(self, proc):
108 """Remove a completed ssh session."""
109 try:
110 self._clients.remove(proc.pid)
111 except ValueError:
112 pass
113
114 def add_master(self, proc):
115 """Track a new master connection."""
116 self._masters.append(proc.pid)
117
118 def _terminate(self, procs):
119 """Kill all |procs|."""
120 for pid in procs:
121 try:
122 os.kill(pid, signal.SIGTERM)
123 os.waitpid(pid, 0)
124 except OSError:
125 pass
126
127 # The multiprocessing.list() API doesn't provide many standard list()
128 # methods, so we have to manually clear the list.
129 while True:
130 try:
131 procs.pop(0)
132 except:
133 break
134
135 def close(self):
136 """Close this active ssh session.
137
138 Kill all ssh clients & masters we created, and nuke the socket dir.
139 """
140 self._terminate(self._clients)
141 self._terminate(self._masters)
142
143 d = self.sock(create=False)
144 if d:
145 try:
146 platform_utils.rmdir(os.path.dirname(d))
147 except OSError:
148 pass
149
150 def _open_unlocked(self, host, port=None):
151 """Make sure a ssh master session exists for |host| & |port|.
152
153 If one doesn't exist already, we'll create it.
154
155 We won't grab any locks, so the caller has to do that. This helps keep the
156 business logic of actually creating the master separate from grabbing locks.
157 """
158 # Check to see whether we already think that the master is running; if we
159 # think it's already running, return right away.
160 if port is not None:
161 key = '%s:%s' % (host, port)
162 else:
163 key = host
164
165 if key in self._master_keys:
166 return True
167
168 if self._master_broken.value or 'GIT_SSH' in os.environ:
169 # Failed earlier, so don't retry.
170 return False
171
172 # We will make two calls to ssh; this is the common part of both calls.
173 command_base = ['ssh', '-o', 'ControlPath %s' % self.sock(), host]
174 if port is not None:
175 command_base[1:1] = ['-p', str(port)]
176
177 # Since the key wasn't in _master_keys, we think that master isn't running.
178 # ...but before actually starting a master, we'll double-check. This can
179 # be important because we can't tell that that 'git@myhost.com' is the same
180 # as 'myhost.com' where "User git" is setup in the user's ~/.ssh/config file.
181 check_command = command_base + ['-O', 'check']
182 try:
183 Trace(': %s', ' '.join(check_command))
184 check_process = subprocess.Popen(check_command,
185 stdout=subprocess.PIPE,
186 stderr=subprocess.PIPE)
187 check_process.communicate() # read output, but ignore it...
188 isnt_running = check_process.wait()
189
190 if not isnt_running:
191 # Our double-check found that the master _was_ infact running. Add to
192 # the list of keys.
193 self._master_keys[key] = True
194 return True
195 except Exception:
196 # Ignore excpetions. We we will fall back to the normal command and print
197 # to the log there.
198 pass
199
200 command = command_base[:1] + ['-M', '-N'] + command_base[1:]
201 try:
202 Trace(': %s', ' '.join(command))
203 p = subprocess.Popen(command)
204 except Exception as e:
205 self._master_broken.value = True
206 print('\nwarn: cannot enable ssh control master for %s:%s\n%s'
207 % (host, port, str(e)), file=sys.stderr)
208 return False
209
210 time.sleep(1)
211 ssh_died = (p.poll() is not None)
212 if ssh_died:
213 return False
214
215 self.add_master(p)
216 self._master_keys[key] = True
217 return True
218
219 def _open(self, host, port=None):
220 """Make sure a ssh master session exists for |host| & |port|.
221
222 If one doesn't exist already, we'll create it.
223
224 This will obtain any necessary locks to avoid inter-process races.
225 """
226 # Bail before grabbing the lock if we already know that we aren't going to
227 # try creating new masters below.
228 if sys.platform in ('win32', 'cygwin'):
229 return False
230
231 # Acquire the lock. This is needed to prevent opening multiple masters for
232 # the same host when we're running "repo sync -jN" (for N > 1) _and_ the
233 # manifest <remote fetch="ssh://xyz"> specifies a different host from the
234 # one that was passed to repo init.
235 with self._lock:
236 return self._open_unlocked(host, port)
237
238 def preconnect(self, url):
239 """If |uri| will create a ssh connection, setup the ssh master for it."""
240 m = URI_ALL.match(url)
241 if m:
242 scheme = m.group(1)
243 host = m.group(2)
244 if ':' in host:
245 host, port = host.split(':')
246 else:
247 port = None
248 if scheme in ('ssh', 'git+ssh', 'ssh+git'):
249 return self._open(host, port)
250 return False
251
252 m = URI_SCP.match(url)
253 if m:
254 host = m.group(1)
255 return self._open(host)
256
257 return False
258
259 def sock(self, create=True):
260 """Return the path to the ssh socket dir.
261
262 This has all the master sockets so clients can talk to them.
263 """
264 if self._sock_path is None:
265 if not create:
266 return None
267 tmp_dir = '/tmp'
268 if not os.path.exists(tmp_dir):
269 tmp_dir = tempfile.gettempdir()
270 if version() < (6, 7):
271 tokens = '%r@%h:%p'
272 else:
273 tokens = '%C' # hash of %l%h%p%r
274 self._sock_path = os.path.join(
275 tempfile.mkdtemp('', 'ssh-', tmp_dir),
276 'master-' + tokens)
277 return self._sock_path
diff --git a/subcmds/__init__.py b/subcmds/__init__.py
index 27341038..051dda06 100644
--- a/subcmds/__init__.py
+++ b/subcmds/__init__.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +14,7 @@
16 14
17import os 15import os
18 16
17# A mapping of the subcommand name to the class that implements it.
19all_commands = {} 18all_commands = {}
20 19
21my_dir = os.path.dirname(__file__) 20my_dir = os.path.dirname(__file__)
@@ -37,14 +36,14 @@ for py in os.listdir(my_dir):
37 ['%s' % name]) 36 ['%s' % name])
38 mod = getattr(mod, name) 37 mod = getattr(mod, name)
39 try: 38 try:
40 cmd = getattr(mod, clsn)() 39 cmd = getattr(mod, clsn)
41 except AttributeError: 40 except AttributeError:
42 raise SyntaxError('%s/%s does not define class %s' % ( 41 raise SyntaxError('%s/%s does not define class %s' % (
43 __name__, py, clsn)) 42 __name__, py, clsn))
44 43
45 name = name.replace('_', '-') 44 name = name.replace('_', '-')
46 cmd.NAME = name 45 cmd.NAME = name
47 all_commands[name] = cmd 46 all_commands[name] = cmd
48 47
49if 'help' in all_commands: 48# Add 'branch' as an alias for 'branches'.
50 all_commands['help'].commands = all_commands 49all_commands['branch'] = all_commands['branches']
diff --git a/subcmds/abandon.py b/subcmds/abandon.py
index cd1d0c40..85d85f5a 100644
--- a/subcmds/abandon.py
+++ b/subcmds/abandon.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,15 +12,18 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function
18import sys
19from command import Command
20from collections import defaultdict 15from collections import defaultdict
16import functools
17import itertools
18import sys
19
20from command import Command, DEFAULT_LOCAL_JOBS
21from git_command import git 21from git_command import git
22from progress import Progress 22from progress import Progress
23 23
24
24class Abandon(Command): 25class Abandon(Command):
25 common = True 26 COMMON = True
26 helpSummary = "Permanently abandon a development branch" 27 helpSummary = "Permanently abandon a development branch"
27 helpUsage = """ 28 helpUsage = """
28%prog [--all | <branchname>] [<project>...] 29%prog [--all | <branchname>] [<project>...]
@@ -32,6 +33,8 @@ deleting it (and all its history) from your local repository.
32 33
33It is equivalent to "git branch -D <branchname>". 34It is equivalent to "git branch -D <branchname>".
34""" 35"""
36 PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
37
35 def _Options(self, p): 38 def _Options(self, p):
36 p.add_option('--all', 39 p.add_option('--all',
37 dest='all', action='store_true', 40 dest='all', action='store_true',
@@ -48,52 +51,64 @@ It is equivalent to "git branch -D <branchname>".
48 else: 51 else:
49 args.insert(0, "'All local branches'") 52 args.insert(0, "'All local branches'")
50 53
54 def _ExecuteOne(self, all_branches, nb, project):
55 """Abandon one project."""
56 if all_branches:
57 branches = project.GetBranches()
58 else:
59 branches = [nb]
60
61 ret = {}
62 for name in branches:
63 status = project.AbandonBranch(name)
64 if status is not None:
65 ret[name] = status
66 return (ret, project)
67
51 def Execute(self, opt, args): 68 def Execute(self, opt, args):
52 nb = args[0] 69 nb = args[0]
53 err = defaultdict(list) 70 err = defaultdict(list)
54 success = defaultdict(list) 71 success = defaultdict(list)
55 all_projects = self.GetProjects(args[1:]) 72 all_projects = self.GetProjects(args[1:])
56 73
57 pm = Progress('Abandon %s' % nb, len(all_projects)) 74 def _ProcessResults(_pool, pm, states):
58 for project in all_projects: 75 for (results, project) in states:
59 pm.update() 76 for branch, status in results.items():
60
61 if opt.all:
62 branches = list(project.GetBranches().keys())
63 else:
64 branches = [nb]
65
66 for name in branches:
67 status = project.AbandonBranch(name)
68 if status is not None:
69 if status: 77 if status:
70 success[name].append(project) 78 success[branch].append(project)
71 else: 79 else:
72 err[name].append(project) 80 err[branch].append(project)
73 pm.end() 81 pm.update()
74 82
75 width = 25 83 self.ExecuteInParallel(
76 for name in branches: 84 opt.jobs,
77 if width < len(name): 85 functools.partial(self._ExecuteOne, opt.all, nb),
78 width = len(name) 86 all_projects,
87 callback=_ProcessResults,
88 output=Progress('Abandon %s' % (nb,), len(all_projects), quiet=opt.quiet))
79 89
90 width = max(itertools.chain(
91 [25], (len(x) for x in itertools.chain(success, err))))
80 if err: 92 if err:
81 for br in err.keys(): 93 for br in err.keys():
82 err_msg = "error: cannot abandon %s" %br 94 err_msg = "error: cannot abandon %s" % br
83 print(err_msg, file=sys.stderr) 95 print(err_msg, file=sys.stderr)
84 for proj in err[br]: 96 for proj in err[br]:
85 print(' '*len(err_msg) + " | %s" % proj.relpath, file=sys.stderr) 97 print(' ' * len(err_msg) + " | %s" % proj.relpath, file=sys.stderr)
86 sys.exit(1) 98 sys.exit(1)
87 elif not success: 99 elif not success:
88 print('error: no project has local branch(es) : %s' % nb, 100 print('error: no project has local branch(es) : %s' % nb,
89 file=sys.stderr) 101 file=sys.stderr)
90 sys.exit(1) 102 sys.exit(1)
91 else: 103 else:
92 print('Abandoned branches:', file=sys.stderr) 104 # Everything below here is displaying status.
105 if opt.quiet:
106 return
107 print('Abandoned branches:')
93 for br in success.keys(): 108 for br in success.keys():
94 if len(all_projects) > 1 and len(all_projects) == len(success[br]): 109 if len(all_projects) > 1 and len(all_projects) == len(success[br]):
95 result = "all project" 110 result = "all project"
96 else: 111 else:
97 result = "%s" % ( 112 result = "%s" % (
98 ('\n'+' '*width + '| ').join(p.relpath for p in success[br])) 113 ('\n' + ' ' * width + '| ').join(p.relpath for p in success[br]))
99 print("%s%s| %s\n" % (br,' '*(width-len(br)), result),file=sys.stderr) 114 print("%s%s| %s\n" % (br, ' ' * (width - len(br)), result))
diff --git a/subcmds/branches.py b/subcmds/branches.py
index fb60d7de..6d975ed4 100644
--- a/subcmds/branches.py
+++ b/subcmds/branches.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2009 The Android Open Source Project 1# Copyright (C) 2009 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,18 +12,21 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function 15import itertools
18import sys 16import sys
17
19from color import Coloring 18from color import Coloring
20from command import Command 19from command import Command, DEFAULT_LOCAL_JOBS
20
21 21
22class BranchColoring(Coloring): 22class BranchColoring(Coloring):
23 def __init__(self, config): 23 def __init__(self, config):
24 Coloring.__init__(self, config, 'branch') 24 Coloring.__init__(self, config, 'branch')
25 self.current = self.printer('current', fg='green') 25 self.current = self.printer('current', fg='green')
26 self.local = self.printer('local') 26 self.local = self.printer('local')
27 self.notinproject = self.printer('notinproject', fg='red') 27 self.notinproject = self.printer('notinproject', fg='red')
28 28
29
29class BranchInfo(object): 30class BranchInfo(object):
30 def __init__(self, name): 31 def __init__(self, name):
31 self.name = name 32 self.name = name
@@ -61,7 +62,7 @@ class BranchInfo(object):
61 62
62 63
63class Branches(Command): 64class Branches(Command):
64 common = True 65 COMMON = True
65 helpSummary = "View current topic branches" 66 helpSummary = "View current topic branches"
66 helpUsage = """ 67 helpUsage = """
67%prog [<project>...] 68%prog [<project>...]
@@ -94,6 +95,7 @@ the branch appears in, or does not appear in. If no project list
94is shown, then the branch appears in all projects. 95is shown, then the branch appears in all projects.
95 96
96""" 97"""
98 PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
97 99
98 def Execute(self, opt, args): 100 def Execute(self, opt, args):
99 projects = self.GetProjects(args) 101 projects = self.GetProjects(args)
@@ -101,14 +103,19 @@ is shown, then the branch appears in all projects.
101 all_branches = {} 103 all_branches = {}
102 project_cnt = len(projects) 104 project_cnt = len(projects)
103 105
104 for project in projects: 106 def _ProcessResults(_pool, _output, results):
105 for name, b in project.GetBranches().items(): 107 for name, b in itertools.chain.from_iterable(results):
106 b.project = project
107 if name not in all_branches: 108 if name not in all_branches:
108 all_branches[name] = BranchInfo(name) 109 all_branches[name] = BranchInfo(name)
109 all_branches[name].add(b) 110 all_branches[name].add(b)
110 111
111 names = list(sorted(all_branches)) 112 self.ExecuteInParallel(
113 opt.jobs,
114 expand_project_to_branches,
115 projects,
116 callback=_ProcessResults)
117
118 names = sorted(all_branches)
112 119
113 if not names: 120 if not names:
114 print(' (no branches)', file=sys.stderr) 121 print(' (no branches)', file=sys.stderr)
@@ -158,7 +165,7 @@ is shown, then the branch appears in all projects.
158 for b in i.projects: 165 for b in i.projects:
159 have.add(b.project) 166 have.add(b.project)
160 for p in projects: 167 for p in projects:
161 if not p in have: 168 if p not in have:
162 paths.append(p.relpath) 169 paths.append(p.relpath)
163 170
164 s = ' %s %s' % (in_type, ', '.join(paths)) 171 s = ' %s %s' % (in_type, ', '.join(paths))
@@ -170,11 +177,27 @@ is shown, then the branch appears in all projects.
170 fmt = out.current if i.IsCurrent else out.write 177 fmt = out.current if i.IsCurrent else out.write
171 for p in paths: 178 for p in paths:
172 out.nl() 179 out.nl()
173 fmt(width*' ' + ' %s' % p) 180 fmt(width * ' ' + ' %s' % p)
174 fmt = out.write 181 fmt = out.write
175 for p in non_cur_paths: 182 for p in non_cur_paths:
176 out.nl() 183 out.nl()
177 fmt(width*' ' + ' %s' % p) 184 fmt(width * ' ' + ' %s' % p)
178 else: 185 else:
179 out.write(' in all projects') 186 out.write(' in all projects')
180 out.nl() 187 out.nl()
188
189
190def expand_project_to_branches(project):
191 """Expands a project into a list of branch names & associated information.
192
193 Args:
194 project: project.Project
195
196 Returns:
197 List[Tuple[str, git_config.Branch]]
198 """
199 branches = []
200 for name, b in project.GetBranches().items():
201 b.project = project
202 branches.append((name, b))
203 return branches
diff --git a/subcmds/checkout.py b/subcmds/checkout.py
index c8a09a8e..9b429489 100644
--- a/subcmds/checkout.py
+++ b/subcmds/checkout.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2009 The Android Open Source Project 1# Copyright (C) 2009 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,13 +12,15 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function 15import functools
18import sys 16import sys
19from command import Command 17
18from command import Command, DEFAULT_LOCAL_JOBS
20from progress import Progress 19from progress import Progress
21 20
21
22class Checkout(Command): 22class Checkout(Command):
23 common = True 23 COMMON = True
24 helpSummary = "Checkout a branch for development" 24 helpSummary = "Checkout a branch for development"
25 helpUsage = """ 25 helpUsage = """
26%prog <branchname> [<project>...] 26%prog <branchname> [<project>...]
@@ -33,28 +33,37 @@ The command is equivalent to:
33 33
34 repo forall [<project>...] -c git checkout <branchname> 34 repo forall [<project>...] -c git checkout <branchname>
35""" 35"""
36 PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
36 37
37 def ValidateOptions(self, opt, args): 38 def ValidateOptions(self, opt, args):
38 if not args: 39 if not args:
39 self.Usage() 40 self.Usage()
40 41
42 def _ExecuteOne(self, nb, project):
43 """Checkout one project."""
44 return (project.CheckoutBranch(nb), project)
45
41 def Execute(self, opt, args): 46 def Execute(self, opt, args):
42 nb = args[0] 47 nb = args[0]
43 err = [] 48 err = []
44 success = [] 49 success = []
45 all_projects = self.GetProjects(args[1:]) 50 all_projects = self.GetProjects(args[1:])
46 51
47 pm = Progress('Checkout %s' % nb, len(all_projects)) 52 def _ProcessResults(_pool, pm, results):
48 for project in all_projects: 53 for status, project in results:
49 pm.update() 54 if status is not None:
55 if status:
56 success.append(project)
57 else:
58 err.append(project)
59 pm.update()
50 60
51 status = project.CheckoutBranch(nb) 61 self.ExecuteInParallel(
52 if status is not None: 62 opt.jobs,
53 if status: 63 functools.partial(self._ExecuteOne, nb),
54 success.append(project) 64 all_projects,
55 else: 65 callback=_ProcessResults,
56 err.append(project) 66 output=Progress('Checkout %s' % (nb,), len(all_projects), quiet=opt.quiet))
57 pm.end()
58 67
59 if err: 68 if err:
60 for p in err: 69 for p in err:
diff --git a/subcmds/cherry_pick.py b/subcmds/cherry_pick.py
index a541a040..7bd858bf 100644
--- a/subcmds/cherry_pick.py
+++ b/subcmds/cherry_pick.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2010 The Android Open Source Project 1# Copyright (C) 2010 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,7 +12,6 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function
18import re 15import re
19import sys 16import sys
20from command import Command 17from command import Command
@@ -22,8 +19,9 @@ from git_command import GitCommand
22 19
23CHANGE_ID_RE = re.compile(r'^\s*Change-Id: I([0-9a-f]{40})\s*$') 20CHANGE_ID_RE = re.compile(r'^\s*Change-Id: I([0-9a-f]{40})\s*$')
24 21
22
25class CherryPick(Command): 23class CherryPick(Command):
26 common = True 24 COMMON = True
27 helpSummary = "Cherry-pick a change." 25 helpSummary = "Cherry-pick a change."
28 helpUsage = """ 26 helpUsage = """
29%prog <sha1> 27%prog <sha1>
@@ -34,9 +32,6 @@ The change id will be updated, and a reference to the old
34change id will be added. 32change id will be added.
35""" 33"""
36 34
37 def _Options(self, p):
38 pass
39
40 def ValidateOptions(self, opt, args): 35 def ValidateOptions(self, opt, args):
41 if len(args) != 1: 36 if len(args) != 1:
42 self.Usage() 37 self.Usage()
@@ -46,8 +41,8 @@ change id will be added.
46 41
47 p = GitCommand(None, 42 p = GitCommand(None,
48 ['rev-parse', '--verify', reference], 43 ['rev-parse', '--verify', reference],
49 capture_stdout = True, 44 capture_stdout=True,
50 capture_stderr = True) 45 capture_stderr=True)
51 if p.Wait() != 0: 46 if p.Wait() != 0:
52 print(p.stderr, file=sys.stderr) 47 print(p.stderr, file=sys.stderr)
53 sys.exit(1) 48 sys.exit(1)
@@ -61,8 +56,8 @@ change id will be added.
61 56
62 p = GitCommand(None, 57 p = GitCommand(None,
63 ['cherry-pick', sha1], 58 ['cherry-pick', sha1],
64 capture_stdout = True, 59 capture_stdout=True,
65 capture_stderr = True) 60 capture_stderr=True)
66 status = p.Wait() 61 status = p.Wait()
67 62
68 print(p.stdout, file=sys.stdout) 63 print(p.stdout, file=sys.stdout)
@@ -74,11 +69,9 @@ change id will be added.
74 new_msg = self._Reformat(old_msg, sha1) 69 new_msg = self._Reformat(old_msg, sha1)
75 70
76 p = GitCommand(None, ['commit', '--amend', '-F', '-'], 71 p = GitCommand(None, ['commit', '--amend', '-F', '-'],
77 provide_stdin = True, 72 input=new_msg,
78 capture_stdout = True, 73 capture_stdout=True,
79 capture_stderr = True) 74 capture_stderr=True)
80 p.stdin.write(new_msg)
81 p.stdin.close()
82 if p.Wait() != 0: 75 if p.Wait() != 0:
83 print("error: Failed to update commit message", file=sys.stderr) 76 print("error: Failed to update commit message", file=sys.stderr)
84 sys.exit(1) 77 sys.exit(1)
@@ -97,7 +90,7 @@ change id will be added.
97 90
98 def _StripHeader(self, commit_msg): 91 def _StripHeader(self, commit_msg):
99 lines = commit_msg.splitlines() 92 lines = commit_msg.splitlines()
100 return "\n".join(lines[lines.index("")+1:]) 93 return "\n".join(lines[lines.index("") + 1:])
101 94
102 def _Reformat(self, old_msg, sha1): 95 def _Reformat(self, old_msg, sha1):
103 new_msg = [] 96 new_msg = []
diff --git a/subcmds/diff.py b/subcmds/diff.py
index fa41e70e..00a7ec29 100644
--- a/subcmds/diff.py
+++ b/subcmds/diff.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,10 +12,14 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from command import PagedCommand 15import functools
16import io
17
18from command import DEFAULT_LOCAL_JOBS, PagedCommand
19
18 20
19class Diff(PagedCommand): 21class Diff(PagedCommand):
20 common = True 22 COMMON = True
21 helpSummary = "Show changes between commit and working tree" 23 helpSummary = "Show changes between commit and working tree"
22 helpUsage = """ 24 helpUsage = """
23%prog [<project>...] 25%prog [<project>...]
@@ -26,19 +28,42 @@ The -u option causes '%prog' to generate diff output with file paths
26relative to the repository root, so the output can be applied 28relative to the repository root, so the output can be applied
27to the Unix 'patch' command. 29to the Unix 'patch' command.
28""" 30"""
31 PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
29 32
30 def _Options(self, p): 33 def _Options(self, p):
31 def cmd(option, opt_str, value, parser):
32 setattr(parser.values, option.dest, list(parser.rargs))
33 while parser.rargs:
34 del parser.rargs[0]
35 p.add_option('-u', '--absolute', 34 p.add_option('-u', '--absolute',
36 dest='absolute', action='store_true', 35 dest='absolute', action='store_true',
37 help='Paths are relative to the repository root') 36 help='paths are relative to the repository root')
37
38 def _ExecuteOne(self, absolute, project):
39 """Obtains the diff for a specific project.
40
41 Args:
42 absolute: Paths are relative to the root.
43 project: Project to get status of.
44
45 Returns:
46 The status of the project.
47 """
48 buf = io.StringIO()
49 ret = project.PrintWorkTreeDiff(absolute, output_redir=buf)
50 return (ret, buf.getvalue())
38 51
39 def Execute(self, opt, args): 52 def Execute(self, opt, args):
40 ret = 0 53 all_projects = self.GetProjects(args)
41 for project in self.GetProjects(args): 54
42 if not project.PrintWorkTreeDiff(opt.absolute): 55 def _ProcessResults(_pool, _output, results):
43 ret = 1 56 ret = 0
44 return ret 57 for (state, output) in results:
58 if output:
59 print(output, end='')
60 if not state:
61 ret = 1
62 return ret
63
64 return self.ExecuteInParallel(
65 opt.jobs,
66 functools.partial(self._ExecuteOne, opt.absolute),
67 all_projects,
68 callback=_ProcessResults,
69 ordered=True)
diff --git a/subcmds/diffmanifests.py b/subcmds/diffmanifests.py
index b999699e..f6cc30a2 100644
--- a/subcmds/diffmanifests.py
+++ b/subcmds/diffmanifests.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2014 The Android Open Source Project 1# Copyright (C) 2014 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,12 +14,14 @@
16 14
17from color import Coloring 15from color import Coloring
18from command import PagedCommand 16from command import PagedCommand
19from manifest_xml import XmlManifest 17from manifest_xml import RepoClient
18
20 19
21class _Coloring(Coloring): 20class _Coloring(Coloring):
22 def __init__(self, config): 21 def __init__(self, config):
23 Coloring.__init__(self, config, "status") 22 Coloring.__init__(self, config, "status")
24 23
24
25class Diffmanifests(PagedCommand): 25class Diffmanifests(PagedCommand):
26 """ A command to see logs in projects represented by manifests 26 """ A command to see logs in projects represented by manifests
27 27
@@ -31,7 +31,7 @@ class Diffmanifests(PagedCommand):
31 deeper level. 31 deeper level.
32 """ 32 """
33 33
34 common = True 34 COMMON = True
35 helpSummary = "Manifest diff utility" 35 helpSummary = "Manifest diff utility"
36 helpUsage = """%prog manifest1.xml [manifest2.xml] [options]""" 36 helpUsage = """%prog manifest1.xml [manifest2.xml] [options]"""
37 37
@@ -68,16 +68,16 @@ synced and their revisions won't be found.
68 def _Options(self, p): 68 def _Options(self, p):
69 p.add_option('--raw', 69 p.add_option('--raw',
70 dest='raw', action='store_true', 70 dest='raw', action='store_true',
71 help='Display raw diff.') 71 help='display raw diff')
72 p.add_option('--no-color', 72 p.add_option('--no-color',
73 dest='color', action='store_false', default=True, 73 dest='color', action='store_false', default=True,
74 help='does not display the diff in color.') 74 help='does not display the diff in color')
75 p.add_option('--pretty-format', 75 p.add_option('--pretty-format',
76 dest='pretty_format', action='store', 76 dest='pretty_format', action='store',
77 metavar='<FORMAT>', 77 metavar='<FORMAT>',
78 help='print the log using a custom git pretty format string') 78 help='print the log using a custom git pretty format string')
79 79
80 def _printRawDiff(self, diff): 80 def _printRawDiff(self, diff, pretty_format=None):
81 for project in diff['added']: 81 for project in diff['added']:
82 self.printText("A %s %s" % (project.relpath, project.revisionExpr)) 82 self.printText("A %s %s" % (project.relpath, project.revisionExpr))
83 self.out.nl() 83 self.out.nl()
@@ -90,7 +90,7 @@ synced and their revisions won't be found.
90 self.printText("C %s %s %s" % (project.relpath, project.revisionExpr, 90 self.printText("C %s %s %s" % (project.relpath, project.revisionExpr,
91 otherProject.revisionExpr)) 91 otherProject.revisionExpr))
92 self.out.nl() 92 self.out.nl()
93 self._printLogs(project, otherProject, raw=True, color=False) 93 self._printLogs(project, otherProject, raw=True, color=False, pretty_format=pretty_format)
94 94
95 for project, otherProject in diff['unreachable']: 95 for project, otherProject in diff['unreachable']:
96 self.printText("U %s %s %s" % (project.relpath, project.revisionExpr, 96 self.printText("U %s %s %s" % (project.relpath, project.revisionExpr,
@@ -181,26 +181,26 @@ synced and their revisions won't be found.
181 self.OptionParser.error('missing manifests to diff') 181 self.OptionParser.error('missing manifests to diff')
182 182
183 def Execute(self, opt, args): 183 def Execute(self, opt, args):
184 self.out = _Coloring(self.manifest.globalConfig) 184 self.out = _Coloring(self.client.globalConfig)
185 self.printText = self.out.nofmt_printer('text') 185 self.printText = self.out.nofmt_printer('text')
186 if opt.color: 186 if opt.color:
187 self.printProject = self.out.nofmt_printer('project', attr = 'bold') 187 self.printProject = self.out.nofmt_printer('project', attr='bold')
188 self.printAdded = self.out.nofmt_printer('green', fg = 'green', attr = 'bold') 188 self.printAdded = self.out.nofmt_printer('green', fg='green', attr='bold')
189 self.printRemoved = self.out.nofmt_printer('red', fg = 'red', attr = 'bold') 189 self.printRemoved = self.out.nofmt_printer('red', fg='red', attr='bold')
190 self.printRevision = self.out.nofmt_printer('revision', fg = 'yellow') 190 self.printRevision = self.out.nofmt_printer('revision', fg='yellow')
191 else: 191 else:
192 self.printProject = self.printAdded = self.printRemoved = self.printRevision = self.printText 192 self.printProject = self.printAdded = self.printRemoved = self.printRevision = self.printText
193 193
194 manifest1 = XmlManifest(self.manifest.repodir) 194 manifest1 = RepoClient(self.repodir)
195 manifest1.Override(args[0], load_local_manifests=False) 195 manifest1.Override(args[0], load_local_manifests=False)
196 if len(args) == 1: 196 if len(args) == 1:
197 manifest2 = self.manifest 197 manifest2 = self.manifest
198 else: 198 else:
199 manifest2 = XmlManifest(self.manifest.repodir) 199 manifest2 = RepoClient(self.repodir)
200 manifest2.Override(args[1], load_local_manifests=False) 200 manifest2.Override(args[1], load_local_manifests=False)
201 201
202 diff = manifest1.projectsDiff(manifest2) 202 diff = manifest1.projectsDiff(manifest2)
203 if opt.raw: 203 if opt.raw:
204 self._printRawDiff(diff) 204 self._printRawDiff(diff, pretty_format=opt.pretty_format)
205 else: 205 else:
206 self._printDiff(diff, color=opt.color, pretty_format=opt.pretty_format) 206 self._printDiff(diff, color=opt.color, pretty_format=opt.pretty_format)
diff --git a/subcmds/download.py b/subcmds/download.py
index f746bc23..523f25e0 100644
--- a/subcmds/download.py
+++ b/subcmds/download.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,17 +12,17 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function
18import re 15import re
19import sys 16import sys
20 17
21from command import Command 18from command import Command
22from error import GitError 19from error import GitError, NoSuchProjectError
23 20
24CHANGE_RE = re.compile(r'^([1-9][0-9]*)(?:[/\.-]([1-9][0-9]*))?$') 21CHANGE_RE = re.compile(r'^([1-9][0-9]*)(?:[/\.-]([1-9][0-9]*))?$')
25 22
23
26class Download(Command): 24class Download(Command):
27 common = True 25 COMMON = True
28 helpSummary = "Download and checkout a change" 26 helpSummary = "Download and checkout a change"
29 helpUsage = """ 27 helpUsage = """
30%prog {[project] change[/patchset]}... 28%prog {[project] change[/patchset]}...
@@ -36,9 +34,13 @@ If no project is specified try to use current directory as a project.
36""" 34"""
37 35
38 def _Options(self, p): 36 def _Options(self, p):
37 p.add_option('-b', '--branch',
38 help='create a new branch first')
39 p.add_option('-c', '--cherry-pick', 39 p.add_option('-c', '--cherry-pick',
40 dest='cherrypick', action='store_true', 40 dest='cherrypick', action='store_true',
41 help="cherry-pick instead of checkout") 41 help="cherry-pick instead of checkout")
42 p.add_option('-x', '--record-origin', action='store_true',
43 help='pass -x when cherry-picking')
42 p.add_option('-r', '--revert', 44 p.add_option('-r', '--revert',
43 dest='revert', action='store_true', 45 dest='revert', action='store_true',
44 help="revert instead of checkout") 46 help="revert instead of checkout")
@@ -58,6 +60,7 @@ If no project is specified try to use current directory as a project.
58 if m: 60 if m:
59 if not project: 61 if not project:
60 project = self.GetProjects(".")[0] 62 project = self.GetProjects(".")[0]
63 print('Defaulting to cwd project', project.name)
61 chg_id = int(m.group(1)) 64 chg_id = int(m.group(1))
62 if m.group(2): 65 if m.group(2):
63 ps_id = int(m.group(2)) 66 ps_id = int(m.group(2))
@@ -74,9 +77,33 @@ If no project is specified try to use current directory as a project.
74 ps_id = max(int(match.group(1)), ps_id) 77 ps_id = max(int(match.group(1)), ps_id)
75 to_get.append((project, chg_id, ps_id)) 78 to_get.append((project, chg_id, ps_id))
76 else: 79 else:
77 project = self.GetProjects([a])[0] 80 projects = self.GetProjects([a])
81 if len(projects) > 1:
82 # If the cwd is one of the projects, assume they want that.
83 try:
84 project = self.GetProjects('.')[0]
85 except NoSuchProjectError:
86 project = None
87 if project not in projects:
88 print('error: %s matches too many projects; please re-run inside '
89 'the project checkout.' % (a,), file=sys.stderr)
90 for project in projects:
91 print(' %s/ @ %s' % (project.relpath, project.revisionExpr),
92 file=sys.stderr)
93 sys.exit(1)
94 else:
95 project = projects[0]
96 print('Defaulting to cwd project', project.name)
78 return to_get 97 return to_get
79 98
99 def ValidateOptions(self, opt, args):
100 if opt.record_origin:
101 if not opt.cherrypick:
102 self.OptionParser.error('-x only makes sense with --cherry-pick')
103
104 if opt.ffonly:
105 self.OptionParser.error('-x and --ff are mutually exclusive options')
106
80 def Execute(self, opt, args): 107 def Execute(self, opt, args):
81 for project, change_id, ps_id in self._ParseChangeIds(args): 108 for project, change_id, ps_id in self._ParseChangeIds(args):
82 dl = project.DownloadPatchSet(change_id, ps_id) 109 dl = project.DownloadPatchSet(change_id, ps_id)
@@ -93,22 +120,41 @@ If no project is specified try to use current directory as a project.
93 continue 120 continue
94 121
95 if len(dl.commits) > 1: 122 if len(dl.commits) > 1:
96 print('[%s] %d/%d depends on %d unmerged changes:' \ 123 print('[%s] %d/%d depends on %d unmerged changes:'
97 % (project.name, change_id, ps_id, len(dl.commits)), 124 % (project.name, change_id, ps_id, len(dl.commits)),
98 file=sys.stderr) 125 file=sys.stderr)
99 for c in dl.commits: 126 for c in dl.commits:
100 print(' %s' % (c), file=sys.stderr) 127 print(' %s' % (c), file=sys.stderr)
101 if opt.cherrypick:
102 try:
103 project._CherryPick(dl.commit)
104 except GitError:
105 print('[%s] Could not complete the cherry-pick of %s' \
106 % (project.name, dl.commit), file=sys.stderr)
107 sys.exit(1)
108 128
129 if opt.cherrypick:
130 mode = 'cherry-pick'
109 elif opt.revert: 131 elif opt.revert:
110 project._Revert(dl.commit) 132 mode = 'revert'
111 elif opt.ffonly: 133 elif opt.ffonly:
112 project._FastForward(dl.commit, ffonly=True) 134 mode = 'fast-forward merge'
113 else: 135 else:
114 project._Checkout(dl.commit) 136 mode = 'checkout'
137
138 # We'll combine the branch+checkout operation, but all the rest need a
139 # dedicated branch start.
140 if opt.branch and mode != 'checkout':
141 project.StartBranch(opt.branch)
142
143 try:
144 if opt.cherrypick:
145 project._CherryPick(dl.commit, ffonly=opt.ffonly,
146 record_origin=opt.record_origin)
147 elif opt.revert:
148 project._Revert(dl.commit)
149 elif opt.ffonly:
150 project._FastForward(dl.commit, ffonly=True)
151 else:
152 if opt.branch:
153 project.StartBranch(opt.branch, revision=dl.commit)
154 else:
155 project._Checkout(dl.commit)
156
157 except GitError:
158 print('[%s] Could not complete the %s of %s'
159 % (project.name, mode, dl.commit), file=sys.stderr)
160 sys.exit(1)
diff --git a/subcmds/forall.py b/subcmds/forall.py
index 131ba676..7c1dea9e 100644
--- a/subcmds/forall.py
+++ b/subcmds/forall.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,8 +12,9 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function
18import errno 15import errno
16import functools
17import io
19import multiprocessing 18import multiprocessing
20import re 19import re
21import os 20import os
@@ -24,14 +23,14 @@ import sys
24import subprocess 23import subprocess
25 24
26from color import Coloring 25from color import Coloring
27from command import Command, MirrorSafeCommand 26from command import DEFAULT_LOCAL_JOBS, Command, MirrorSafeCommand, WORKER_BATCH_SIZE
28import platform_utils 27from error import ManifestInvalidRevisionError
29 28
30_CAN_COLOR = [ 29_CAN_COLOR = [
31 'branch', 30 'branch',
32 'diff', 31 'diff',
33 'grep', 32 'grep',
34 'log', 33 'log',
35] 34]
36 35
37 36
@@ -42,11 +41,11 @@ class ForallColoring(Coloring):
42 41
43 42
44class Forall(Command, MirrorSafeCommand): 43class Forall(Command, MirrorSafeCommand):
45 common = False 44 COMMON = False
46 helpSummary = "Run a shell command in each project" 45 helpSummary = "Run a shell command in each project"
47 helpUsage = """ 46 helpUsage = """
48%prog [<project>...] -c <command> [<arg>...] 47%prog [<project>...] -c <command> [<arg>...]
49%prog -r str1 [str2] ... -c <command> [<arg>...]" 48%prog -r str1 [str2] ... -c <command> [<arg>...]
50""" 49"""
51 helpDescription = """ 50 helpDescription = """
52Executes the same shell command in each project. 51Executes the same shell command in each project.
@@ -54,6 +53,11 @@ Executes the same shell command in each project.
54The -r option allows running the command only on projects matching 53The -r option allows running the command only on projects matching
55regex or wildcard expression. 54regex or wildcard expression.
56 55
56By default, projects are processed non-interactively in parallel. If you want
57to run interactive commands, make sure to pass --interactive to force --jobs 1.
58While the processing order of projects is not guaranteed, the order of project
59output is stable.
60
57# Output Formatting 61# Output Formatting
58 62
59The -p option causes '%prog' to bind pipes to the command's stdin, 63The -p option causes '%prog' to bind pipes to the command's stdin,
@@ -116,70 +120,48 @@ terminal and are not redirected.
116If -e is used, when a command exits unsuccessfully, '%prog' will abort 120If -e is used, when a command exits unsuccessfully, '%prog' will abort
117without iterating through the remaining projects. 121without iterating through the remaining projects.
118""" 122"""
123 PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
124
125 @staticmethod
126 def _cmd_option(option, _opt_str, _value, parser):
127 setattr(parser.values, option.dest, list(parser.rargs))
128 while parser.rargs:
129 del parser.rargs[0]
119 130
120 def _Options(self, p): 131 def _Options(self, p):
121 def cmd(option, opt_str, value, parser):
122 setattr(parser.values, option.dest, list(parser.rargs))
123 while parser.rargs:
124 del parser.rargs[0]
125 p.add_option('-r', '--regex', 132 p.add_option('-r', '--regex',
126 dest='regex', action='store_true', 133 dest='regex', action='store_true',
127 help="Execute the command only on projects matching regex or wildcard expression") 134 help='execute the command only on projects matching regex or wildcard expression')
128 p.add_option('-i', '--inverse-regex', 135 p.add_option('-i', '--inverse-regex',
129 dest='inverse_regex', action='store_true', 136 dest='inverse_regex', action='store_true',
130 help="Execute the command only on projects not matching regex or wildcard expression") 137 help='execute the command only on projects not matching regex or '
138 'wildcard expression')
131 p.add_option('-g', '--groups', 139 p.add_option('-g', '--groups',
132 dest='groups', 140 dest='groups',
133 help="Execute the command only on projects matching the specified groups") 141 help='execute the command only on projects matching the specified groups')
134 p.add_option('-c', '--command', 142 p.add_option('-c', '--command',
135 help='Command (and arguments) to execute', 143 help='command (and arguments) to execute',
136 dest='command', 144 dest='command',
137 action='callback', 145 action='callback',
138 callback=cmd) 146 callback=self._cmd_option)
139 p.add_option('-e', '--abort-on-errors', 147 p.add_option('-e', '--abort-on-errors',
140 dest='abort_on_errors', action='store_true', 148 dest='abort_on_errors', action='store_true',
141 help='Abort if a command exits unsuccessfully') 149 help='abort if a command exits unsuccessfully')
142 p.add_option('--ignore-missing', action='store_true', 150 p.add_option('--ignore-missing', action='store_true',
143 help='Silently skip & do not exit non-zero due missing ' 151 help='silently skip & do not exit non-zero due missing '
144 'checkouts') 152 'checkouts')
145 153
146 g = p.add_option_group('Output') 154 g = p.get_option_group('--quiet')
147 g.add_option('-p', 155 g.add_option('-p',
148 dest='project_header', action='store_true', 156 dest='project_header', action='store_true',
149 help='Show project headers before output') 157 help='show project headers before output')
150 g.add_option('-v', '--verbose', 158 p.add_option('--interactive',
151 dest='verbose', action='store_true', 159 action='store_true',
152 help='Show command error messages') 160 help='force interactive usage')
153 g.add_option('-j', '--jobs',
154 dest='jobs', action='store', type='int', default=1,
155 help='number of commands to execute simultaneously')
156 161
157 def WantPager(self, opt): 162 def WantPager(self, opt):
158 return opt.project_header and opt.jobs == 1 163 return opt.project_header and opt.jobs == 1
159 164
160 def _SerializeProject(self, project):
161 """ Serialize a project._GitGetByExec instance.
162
163 project._GitGetByExec is not pickle-able. Instead of trying to pass it
164 around between processes, make a dict ourselves containing only the
165 attributes that we need.
166
167 """
168 if not self.manifest.IsMirror:
169 lrev = project.GetRevisionId()
170 else:
171 lrev = None
172 return {
173 'name': project.name,
174 'relpath': project.relpath,
175 'remote_name': project.remote.name,
176 'lrev': lrev,
177 'rrev': project.revisionExpr,
178 'annotations': dict((a.name, a.value) for a in project.annotations),
179 'gitdir': project.gitdir,
180 'worktree': project.worktree,
181 }
182
183 def ValidateOptions(self, opt, args): 165 def ValidateOptions(self, opt, args):
184 if not opt.command: 166 if not opt.command:
185 self.Usage() 167 self.Usage()
@@ -195,9 +177,14 @@ without iterating through the remaining projects.
195 cmd.append(cmd[0]) 177 cmd.append(cmd[0])
196 cmd.extend(opt.command[1:]) 178 cmd.extend(opt.command[1:])
197 179
198 if opt.project_header \ 180 # Historically, forall operated interactively, and in serial. If the user
199 and not shell \ 181 # has selected 1 job, then default to interacive mode.
200 and cmd[0] == 'git': 182 if opt.jobs == 1:
183 opt.interactive = True
184
185 if opt.project_header \
186 and not shell \
187 and cmd[0] == 'git':
201 # If this is a direct git command that can enable colorized 188 # If this is a direct git command that can enable colorized
202 # output and the user prefers coloring, add --color into the 189 # output and the user prefers coloring, add --color into the
203 # command line because we are going to wrap the command into 190 # command line because we are going to wrap the command into
@@ -220,7 +207,7 @@ without iterating through the remaining projects.
220 207
221 smart_sync_manifest_name = "smart_sync_override.xml" 208 smart_sync_manifest_name = "smart_sync_override.xml"
222 smart_sync_manifest_path = os.path.join( 209 smart_sync_manifest_path = os.path.join(
223 self.manifest.manifestProject.worktree, smart_sync_manifest_name) 210 self.manifest.manifestProject.worktree, smart_sync_manifest_name)
224 211
225 if os.path.isfile(smart_sync_manifest_path): 212 if os.path.isfile(smart_sync_manifest_path):
226 self.manifest.Override(smart_sync_manifest_path) 213 self.manifest.Override(smart_sync_manifest_path)
@@ -234,58 +221,50 @@ without iterating through the remaining projects.
234 221
235 os.environ['REPO_COUNT'] = str(len(projects)) 222 os.environ['REPO_COUNT'] = str(len(projects))
236 223
237 pool = multiprocessing.Pool(opt.jobs, InitWorker)
238 try: 224 try:
239 config = self.manifest.manifestProject.config 225 config = self.manifest.manifestProject.config
240 results_it = pool.imap( 226 with multiprocessing.Pool(opt.jobs, InitWorker) as pool:
241 DoWorkWrapper, 227 results_it = pool.imap(
242 self.ProjectArgs(projects, mirror, opt, cmd, shell, config)) 228 functools.partial(DoWorkWrapper, mirror, opt, cmd, shell, config),
243 pool.close() 229 enumerate(projects),
244 for r in results_it: 230 chunksize=WORKER_BATCH_SIZE)
245 rc = rc or r 231 first = True
246 if r != 0 and opt.abort_on_errors: 232 for (r, output) in results_it:
247 raise Exception('Aborting due to previous error') 233 if output:
234 if first:
235 first = False
236 elif opt.project_header:
237 print()
238 # To simplify the DoWorkWrapper, take care of automatic newlines.
239 end = '\n'
240 if output[-1] == '\n':
241 end = ''
242 print(output, end=end)
243 rc = rc or r
244 if r != 0 and opt.abort_on_errors:
245 raise Exception('Aborting due to previous error')
248 except (KeyboardInterrupt, WorkerKeyboardInterrupt): 246 except (KeyboardInterrupt, WorkerKeyboardInterrupt):
249 # Catch KeyboardInterrupt raised inside and outside of workers 247 # Catch KeyboardInterrupt raised inside and outside of workers
250 print('Interrupted - terminating the pool')
251 pool.terminate()
252 rc = rc or errno.EINTR 248 rc = rc or errno.EINTR
253 except Exception as e: 249 except Exception as e:
254 # Catch any other exceptions raised 250 # Catch any other exceptions raised
255 print('Got an error, terminating the pool: %s: %s' % 251 print('forall: unhandled error, terminating the pool: %s: %s' %
256 (type(e).__name__, e), 252 (type(e).__name__, e),
257 file=sys.stderr) 253 file=sys.stderr)
258 pool.terminate()
259 rc = rc or getattr(e, 'errno', 1) 254 rc = rc or getattr(e, 'errno', 1)
260 finally:
261 pool.join()
262 if rc != 0: 255 if rc != 0:
263 sys.exit(rc) 256 sys.exit(rc)
264 257
265 def ProjectArgs(self, projects, mirror, opt, cmd, shell, config):
266 for cnt, p in enumerate(projects):
267 try:
268 project = self._SerializeProject(p)
269 except Exception as e:
270 print('Project list error on project %s: %s: %s' %
271 (p.name, type(e).__name__, e),
272 file=sys.stderr)
273 return
274 except KeyboardInterrupt:
275 print('Project list interrupted',
276 file=sys.stderr)
277 return
278 yield [mirror, opt, cmd, shell, cnt, config, project]
279 258
280class WorkerKeyboardInterrupt(Exception): 259class WorkerKeyboardInterrupt(Exception):
281 """ Keyboard interrupt exception for worker processes. """ 260 """ Keyboard interrupt exception for worker processes. """
282 pass
283 261
284 262
285def InitWorker(): 263def InitWorker():
286 signal.signal(signal.SIGINT, signal.SIG_IGN) 264 signal.signal(signal.SIGINT, signal.SIG_IGN)
287 265
288def DoWorkWrapper(args): 266
267def DoWorkWrapper(mirror, opt, cmd, shell, config, args):
289 """ A wrapper around the DoWork() method. 268 """ A wrapper around the DoWork() method.
290 269
291 Catch the KeyboardInterrupt exceptions here and re-raise them as a different, 270 Catch the KeyboardInterrupt exceptions here and re-raise them as a different,
@@ -293,109 +272,81 @@ def DoWorkWrapper(args):
293 and making the parent hang indefinitely. 272 and making the parent hang indefinitely.
294 273
295 """ 274 """
296 project = args.pop() 275 cnt, project = args
297 try: 276 try:
298 return DoWork(project, *args) 277 return DoWork(project, mirror, opt, cmd, shell, cnt, config)
299 except KeyboardInterrupt: 278 except KeyboardInterrupt:
300 print('%s: Worker interrupted' % project['name']) 279 print('%s: Worker interrupted' % project.name)
301 raise WorkerKeyboardInterrupt() 280 raise WorkerKeyboardInterrupt()
302 281
303 282
304def DoWork(project, mirror, opt, cmd, shell, cnt, config): 283def DoWork(project, mirror, opt, cmd, shell, cnt, config):
305 env = os.environ.copy() 284 env = os.environ.copy()
285
306 def setenv(name, val): 286 def setenv(name, val):
307 if val is None: 287 if val is None:
308 val = '' 288 val = ''
309 if hasattr(val, 'encode'):
310 val = val.encode()
311 env[name] = val 289 env[name] = val
312 290
313 setenv('REPO_PROJECT', project['name']) 291 setenv('REPO_PROJECT', project.name)
314 setenv('REPO_PATH', project['relpath']) 292 setenv('REPO_PATH', project.relpath)
315 setenv('REPO_REMOTE', project['remote_name']) 293 setenv('REPO_REMOTE', project.remote.name)
316 setenv('REPO_LREV', project['lrev']) 294 try:
317 setenv('REPO_RREV', project['rrev']) 295 # If we aren't in a fully synced state and we don't have the ref the manifest
296 # wants, then this will fail. Ignore it for the purposes of this code.
297 lrev = '' if mirror else project.GetRevisionId()
298 except ManifestInvalidRevisionError:
299 lrev = ''
300 setenv('REPO_LREV', lrev)
301 setenv('REPO_RREV', project.revisionExpr)
302 setenv('REPO_UPSTREAM', project.upstream)
303 setenv('REPO_DEST_BRANCH', project.dest_branch)
318 setenv('REPO_I', str(cnt + 1)) 304 setenv('REPO_I', str(cnt + 1))
319 for name in project['annotations']: 305 for annotation in project.annotations:
320 setenv("REPO__%s" % (name), project['annotations'][name]) 306 setenv("REPO__%s" % (annotation.name), annotation.value)
321 307
322 if mirror: 308 if mirror:
323 setenv('GIT_DIR', project['gitdir']) 309 setenv('GIT_DIR', project.gitdir)
324 cwd = project['gitdir'] 310 cwd = project.gitdir
325 else: 311 else:
326 cwd = project['worktree'] 312 cwd = project.worktree
327 313
328 if not os.path.exists(cwd): 314 if not os.path.exists(cwd):
329 # Allow the user to silently ignore missing checkouts so they can run on 315 # Allow the user to silently ignore missing checkouts so they can run on
330 # partial checkouts (good for infra recovery tools). 316 # partial checkouts (good for infra recovery tools).
331 if opt.ignore_missing: 317 if opt.ignore_missing:
332 return 0 318 return (0, '')
319
320 output = ''
333 if ((opt.project_header and opt.verbose) 321 if ((opt.project_header and opt.verbose)
334 or not opt.project_header): 322 or not opt.project_header):
335 print('skipping %s/' % project['relpath'], file=sys.stderr) 323 output = 'skipping %s/' % project.relpath
336 return 1 324 return (1, output)
337 325
338 if opt.project_header: 326 if opt.verbose:
339 stdin = subprocess.PIPE 327 stderr = subprocess.STDOUT
340 stdout = subprocess.PIPE
341 stderr = subprocess.PIPE
342 else: 328 else:
343 stdin = None 329 stderr = subprocess.DEVNULL
344 stdout = None 330
345 stderr = None 331 stdin = None if opt.interactive else subprocess.DEVNULL
346
347 p = subprocess.Popen(cmd,
348 cwd=cwd,
349 shell=shell,
350 env=env,
351 stdin=stdin,
352 stdout=stdout,
353 stderr=stderr)
354 332
333 result = subprocess.run(
334 cmd, cwd=cwd, shell=shell, env=env, check=False,
335 encoding='utf-8', errors='replace',
336 stdin=stdin, stdout=subprocess.PIPE, stderr=stderr)
337
338 output = result.stdout
355 if opt.project_header: 339 if opt.project_header:
356 out = ForallColoring(config) 340 if output:
357 out.redirect(sys.stdout) 341 buf = io.StringIO()
358 empty = True 342 out = ForallColoring(config)
359 errbuf = '' 343 out.redirect(buf)
360 344 if mirror:
361 p.stdin.close() 345 project_header_path = project.name
362 s_in = platform_utils.FileDescriptorStreams.create() 346 else:
363 s_in.add(p.stdout, sys.stdout, 'stdout') 347 project_header_path = project.relpath
364 s_in.add(p.stderr, sys.stderr, 'stderr') 348 out.project('project %s/' % project_header_path)
365 349 out.nl()
366 while not s_in.is_done: 350 buf.write(output)
367 in_ready = s_in.select() 351 output = buf.getvalue()
368 for s in in_ready: 352 return (result.returncode, output)
369 buf = s.read().decode()
370 if not buf:
371 s.close()
372 s_in.remove(s)
373 continue
374
375 if not opt.verbose:
376 if s.std_name == 'stderr':
377 errbuf += buf
378 continue
379
380 if empty and out:
381 if not cnt == 0:
382 out.nl()
383
384 if mirror:
385 project_header_path = project['name']
386 else:
387 project_header_path = project['relpath']
388 out.project('project %s/', project_header_path)
389 out.nl()
390 out.flush()
391 if errbuf:
392 sys.stderr.write(errbuf)
393 sys.stderr.flush()
394 errbuf = ''
395 empty = False
396
397 s.dest.write(buf)
398 s.dest.flush()
399
400 r = p.wait()
401 return r
diff --git a/subcmds/gitc_delete.py b/subcmds/gitc_delete.py
index e5214b8e..df749469 100644
--- a/subcmds/gitc_delete.py
+++ b/subcmds/gitc_delete.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2015 The Android Open Source Project 1# Copyright (C) 2015 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,18 +12,14 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function
18import sys 15import sys
19 16
20from command import Command, GitcClientCommand 17from command import Command, GitcClientCommand
21import platform_utils 18import platform_utils
22 19
23from pyversion import is_python3
24if not is_python3():
25 input = raw_input
26 20
27class GitcDelete(Command, GitcClientCommand): 21class GitcDelete(Command, GitcClientCommand):
28 common = True 22 COMMON = True
29 visible_everywhere = False 23 visible_everywhere = False
30 helpSummary = "Delete a GITC Client." 24 helpSummary = "Delete a GITC Client."
31 helpUsage = """ 25 helpUsage = """
@@ -39,7 +33,7 @@ and all locally downloaded sources.
39 def _Options(self, p): 33 def _Options(self, p):
40 p.add_option('-f', '--force', 34 p.add_option('-f', '--force',
41 dest='force', action='store_true', 35 dest='force', action='store_true',
42 help='Force the deletion (no prompt).') 36 help='force the deletion (no prompt)')
43 37
44 def Execute(self, opt, args): 38 def Execute(self, opt, args):
45 if not opt.force: 39 if not opt.force:
diff --git a/subcmds/gitc_init.py b/subcmds/gitc_init.py
index 378f9236..e705b613 100644
--- a/subcmds/gitc_init.py
+++ b/subcmds/gitc_init.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2015 The Android Open Source Project 1# Copyright (C) 2015 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,7 +12,6 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function
18import os 15import os
19import sys 16import sys
20 17
@@ -26,7 +23,7 @@ import wrapper
26 23
27 24
28class GitcInit(init.Init, GitcAvailableCommand): 25class GitcInit(init.Init, GitcAvailableCommand):
29 common = True 26 COMMON = True
30 helpSummary = "Initialize a GITC Client." 27 helpSummary = "Initialize a GITC Client."
31 helpUsage = """ 28 helpUsage = """
32%prog [options] [client name] 29%prog [options] [client name]
@@ -50,23 +47,17 @@ use for this GITC client.
50""" 47"""
51 48
52 def _Options(self, p): 49 def _Options(self, p):
53 super(GitcInit, self)._Options(p, gitc_init=True) 50 super()._Options(p, gitc_init=True)
54 g = p.add_option_group('GITC options')
55 g.add_option('-f', '--manifest-file',
56 dest='manifest_file',
57 help='Optional manifest file to use for this GITC client.')
58 g.add_option('-c', '--gitc-client',
59 dest='gitc_client',
60 help='The name of the gitc_client instance to create or modify.')
61 51
62 def Execute(self, opt, args): 52 def Execute(self, opt, args):
63 gitc_client = gitc_utils.parse_clientdir(os.getcwd()) 53 gitc_client = gitc_utils.parse_clientdir(os.getcwd())
64 if not gitc_client or (opt.gitc_client and gitc_client != opt.gitc_client): 54 if not gitc_client or (opt.gitc_client and gitc_client != opt.gitc_client):
65 print('fatal: Please update your repo command. See go/gitc for instructions.', file=sys.stderr) 55 print('fatal: Please update your repo command. See go/gitc for instructions.',
56 file=sys.stderr)
66 sys.exit(1) 57 sys.exit(1)
67 self.client_dir = os.path.join(gitc_utils.get_gitc_manifest_dir(), 58 self.client_dir = os.path.join(gitc_utils.get_gitc_manifest_dir(),
68 gitc_client) 59 gitc_client)
69 super(GitcInit, self).Execute(opt, args) 60 super().Execute(opt, args)
70 61
71 manifest_file = self.manifest.manifestFile 62 manifest_file = self.manifest.manifestFile
72 if opt.manifest_file: 63 if opt.manifest_file:
diff --git a/subcmds/grep.py b/subcmds/grep.py
index 4dd85d57..8ac4ba14 100644
--- a/subcmds/grep.py
+++ b/subcmds/grep.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2009 The Android Open Source Project 1# Copyright (C) 2009 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,14 +12,14 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function 15import functools
18
19import sys 16import sys
20 17
21from color import Coloring 18from color import Coloring
22from command import PagedCommand 19from command import DEFAULT_LOCAL_JOBS, PagedCommand
23from error import GitError 20from error import GitError
24from git_command import git_require, GitCommand 21from git_command import GitCommand
22
25 23
26class GrepColoring(Coloring): 24class GrepColoring(Coloring):
27 def __init__(self, config): 25 def __init__(self, config):
@@ -29,8 +27,9 @@ class GrepColoring(Coloring):
29 self.project = self.printer('project', attr='bold') 27 self.project = self.printer('project', attr='bold')
30 self.fail = self.printer('fail', fg='red') 28 self.fail = self.printer('fail', fg='red')
31 29
30
32class Grep(PagedCommand): 31class Grep(PagedCommand):
33 common = True 32 COMMON = True
34 helpSummary = "Print lines matching a pattern" 33 helpSummary = "Print lines matching a pattern"
35 helpUsage = """ 34 helpUsage = """
36%prog {pattern | -e pattern} [<project>...] 35%prog {pattern | -e pattern} [<project>...]
@@ -63,30 +62,33 @@ contain a line that matches both expressions:
63 repo grep --all-match -e NODE -e Unexpected 62 repo grep --all-match -e NODE -e Unexpected
64 63
65""" 64"""
65 PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
66
67 @staticmethod
68 def _carry_option(_option, opt_str, value, parser):
69 pt = getattr(parser.values, 'cmd_argv', None)
70 if pt is None:
71 pt = []
72 setattr(parser.values, 'cmd_argv', pt)
73
74 if opt_str == '-(':
75 pt.append('(')
76 elif opt_str == '-)':
77 pt.append(')')
78 else:
79 pt.append(opt_str)
66 80
67 def _Options(self, p): 81 if value is not None:
68 def carry(option, 82 pt.append(value)
69 opt_str,
70 value,
71 parser):
72 pt = getattr(parser.values, 'cmd_argv', None)
73 if pt is None:
74 pt = []
75 setattr(parser.values, 'cmd_argv', pt)
76
77 if opt_str == '-(':
78 pt.append('(')
79 elif opt_str == '-)':
80 pt.append(')')
81 else:
82 pt.append(opt_str)
83 83
84 if value is not None: 84 def _CommonOptions(self, p):
85 pt.append(value) 85 """Override common options slightly."""
86 super()._CommonOptions(p, opt_v=False)
86 87
88 def _Options(self, p):
87 g = p.add_option_group('Sources') 89 g = p.add_option_group('Sources')
88 g.add_option('--cached', 90 g.add_option('--cached',
89 action='callback', callback=carry, 91 action='callback', callback=self._carry_option,
90 help='Search the index, instead of the work tree') 92 help='Search the index, instead of the work tree')
91 g.add_option('-r', '--revision', 93 g.add_option('-r', '--revision',
92 dest='revision', action='append', metavar='TREEish', 94 dest='revision', action='append', metavar='TREEish',
@@ -94,136 +96,111 @@ contain a line that matches both expressions:
94 96
95 g = p.add_option_group('Pattern') 97 g = p.add_option_group('Pattern')
96 g.add_option('-e', 98 g.add_option('-e',
97 action='callback', callback=carry, 99 action='callback', callback=self._carry_option,
98 metavar='PATTERN', type='str', 100 metavar='PATTERN', type='str',
99 help='Pattern to search for') 101 help='Pattern to search for')
100 g.add_option('-i', '--ignore-case', 102 g.add_option('-i', '--ignore-case',
101 action='callback', callback=carry, 103 action='callback', callback=self._carry_option,
102 help='Ignore case differences') 104 help='Ignore case differences')
103 g.add_option('-a', '--text', 105 g.add_option('-a', '--text',
104 action='callback', callback=carry, 106 action='callback', callback=self._carry_option,
105 help="Process binary files as if they were text") 107 help="Process binary files as if they were text")
106 g.add_option('-I', 108 g.add_option('-I',
107 action='callback', callback=carry, 109 action='callback', callback=self._carry_option,
108 help="Don't match the pattern in binary files") 110 help="Don't match the pattern in binary files")
109 g.add_option('-w', '--word-regexp', 111 g.add_option('-w', '--word-regexp',
110 action='callback', callback=carry, 112 action='callback', callback=self._carry_option,
111 help='Match the pattern only at word boundaries') 113 help='Match the pattern only at word boundaries')
112 g.add_option('-v', '--invert-match', 114 g.add_option('-v', '--invert-match',
113 action='callback', callback=carry, 115 action='callback', callback=self._carry_option,
114 help='Select non-matching lines') 116 help='Select non-matching lines')
115 g.add_option('-G', '--basic-regexp', 117 g.add_option('-G', '--basic-regexp',
116 action='callback', callback=carry, 118 action='callback', callback=self._carry_option,
117 help='Use POSIX basic regexp for patterns (default)') 119 help='Use POSIX basic regexp for patterns (default)')
118 g.add_option('-E', '--extended-regexp', 120 g.add_option('-E', '--extended-regexp',
119 action='callback', callback=carry, 121 action='callback', callback=self._carry_option,
120 help='Use POSIX extended regexp for patterns') 122 help='Use POSIX extended regexp for patterns')
121 g.add_option('-F', '--fixed-strings', 123 g.add_option('-F', '--fixed-strings',
122 action='callback', callback=carry, 124 action='callback', callback=self._carry_option,
123 help='Use fixed strings (not regexp) for pattern') 125 help='Use fixed strings (not regexp) for pattern')
124 126
125 g = p.add_option_group('Pattern Grouping') 127 g = p.add_option_group('Pattern Grouping')
126 g.add_option('--all-match', 128 g.add_option('--all-match',
127 action='callback', callback=carry, 129 action='callback', callback=self._carry_option,
128 help='Limit match to lines that have all patterns') 130 help='Limit match to lines that have all patterns')
129 g.add_option('--and', '--or', '--not', 131 g.add_option('--and', '--or', '--not',
130 action='callback', callback=carry, 132 action='callback', callback=self._carry_option,
131 help='Boolean operators to combine patterns') 133 help='Boolean operators to combine patterns')
132 g.add_option('-(', '-)', 134 g.add_option('-(', '-)',
133 action='callback', callback=carry, 135 action='callback', callback=self._carry_option,
134 help='Boolean operator grouping') 136 help='Boolean operator grouping')
135 137
136 g = p.add_option_group('Output') 138 g = p.add_option_group('Output')
137 g.add_option('-n', 139 g.add_option('-n',
138 action='callback', callback=carry, 140 action='callback', callback=self._carry_option,
139 help='Prefix the line number to matching lines') 141 help='Prefix the line number to matching lines')
140 g.add_option('-C', 142 g.add_option('-C',
141 action='callback', callback=carry, 143 action='callback', callback=self._carry_option,
142 metavar='CONTEXT', type='str', 144 metavar='CONTEXT', type='str',
143 help='Show CONTEXT lines around match') 145 help='Show CONTEXT lines around match')
144 g.add_option('-B', 146 g.add_option('-B',
145 action='callback', callback=carry, 147 action='callback', callback=self._carry_option,
146 metavar='CONTEXT', type='str', 148 metavar='CONTEXT', type='str',
147 help='Show CONTEXT lines before match') 149 help='Show CONTEXT lines before match')
148 g.add_option('-A', 150 g.add_option('-A',
149 action='callback', callback=carry, 151 action='callback', callback=self._carry_option,
150 metavar='CONTEXT', type='str', 152 metavar='CONTEXT', type='str',
151 help='Show CONTEXT lines after match') 153 help='Show CONTEXT lines after match')
152 g.add_option('-l', '--name-only', '--files-with-matches', 154 g.add_option('-l', '--name-only', '--files-with-matches',
153 action='callback', callback=carry, 155 action='callback', callback=self._carry_option,
154 help='Show only file names containing matching lines') 156 help='Show only file names containing matching lines')
155 g.add_option('-L', '--files-without-match', 157 g.add_option('-L', '--files-without-match',
156 action='callback', callback=carry, 158 action='callback', callback=self._carry_option,
157 help='Show only file names not containing matching lines') 159 help='Show only file names not containing matching lines')
158 160
159 161 def _ExecuteOne(self, cmd_argv, project):
160 def Execute(self, opt, args): 162 """Process one project."""
161 out = GrepColoring(self.manifest.manifestProject.config) 163 try:
162 164 p = GitCommand(project,
163 cmd_argv = ['grep'] 165 cmd_argv,
164 if out.is_on and git_require((1, 6, 3)): 166 bare=False,
165 cmd_argv.append('--color') 167 capture_stdout=True,
166 cmd_argv.extend(getattr(opt, 'cmd_argv', [])) 168 capture_stderr=True)
167 169 except GitError as e:
168 if '-e' not in cmd_argv: 170 return (project, -1, None, str(e))
169 if not args: 171
170 self.Usage() 172 return (project, p.Wait(), p.stdout, p.stderr)
171 cmd_argv.append('-e') 173
172 cmd_argv.append(args[0]) 174 @staticmethod
173 args = args[1:] 175 def _ProcessResults(full_name, have_rev, _pool, out, results):
174
175 projects = self.GetProjects(args)
176
177 full_name = False
178 if len(projects) > 1:
179 cmd_argv.append('--full-name')
180 full_name = True
181
182 have_rev = False
183 if opt.revision:
184 if '--cached' in cmd_argv:
185 print('fatal: cannot combine --cached and --revision', file=sys.stderr)
186 sys.exit(1)
187 have_rev = True
188 cmd_argv.extend(opt.revision)
189 cmd_argv.append('--')
190
191 git_failed = False 176 git_failed = False
192 bad_rev = False 177 bad_rev = False
193 have_match = False 178 have_match = False
194 179
195 for project in projects: 180 for project, rc, stdout, stderr in results:
196 try: 181 if rc < 0:
197 p = GitCommand(project,
198 cmd_argv,
199 bare=False,
200 capture_stdout=True,
201 capture_stderr=True)
202 except GitError as e:
203 git_failed = True 182 git_failed = True
204 out.project('--- project %s ---' % project.relpath) 183 out.project('--- project %s ---' % project.relpath)
205 out.nl() 184 out.nl()
206 out.fail('%s', str(e)) 185 out.fail('%s', stderr)
207 out.nl() 186 out.nl()
208 continue 187 continue
209 188
210 if p.Wait() != 0: 189 if rc:
211 # no results 190 # no results
212 # 191 if stderr:
213 if p.stderr: 192 if have_rev and 'fatal: ambiguous argument' in stderr:
214 if have_rev and 'fatal: ambiguous argument' in p.stderr:
215 bad_rev = True 193 bad_rev = True
216 else: 194 else:
217 out.project('--- project %s ---' % project.relpath) 195 out.project('--- project %s ---' % project.relpath)
218 out.nl() 196 out.nl()
219 out.fail('%s', p.stderr.strip()) 197 out.fail('%s', stderr.strip())
220 out.nl() 198 out.nl()
221 continue 199 continue
222 have_match = True 200 have_match = True
223 201
224 # We cut the last element, to avoid a blank line. 202 # We cut the last element, to avoid a blank line.
225 # 203 r = stdout.split('\n')
226 r = p.stdout.split('\n')
227 r = r[0:-1] 204 r = r[0:-1]
228 205
229 if have_rev and full_name: 206 if have_rev and full_name:
@@ -245,6 +222,47 @@ contain a line that matches both expressions:
245 for line in r: 222 for line in r:
246 print(line) 223 print(line)
247 224
225 return (git_failed, bad_rev, have_match)
226
227 def Execute(self, opt, args):
228 out = GrepColoring(self.manifest.manifestProject.config)
229
230 cmd_argv = ['grep']
231 if out.is_on:
232 cmd_argv.append('--color')
233 cmd_argv.extend(getattr(opt, 'cmd_argv', []))
234
235 if '-e' not in cmd_argv:
236 if not args:
237 self.Usage()
238 cmd_argv.append('-e')
239 cmd_argv.append(args[0])
240 args = args[1:]
241
242 projects = self.GetProjects(args)
243
244 full_name = False
245 if len(projects) > 1:
246 cmd_argv.append('--full-name')
247 full_name = True
248
249 have_rev = False
250 if opt.revision:
251 if '--cached' in cmd_argv:
252 print('fatal: cannot combine --cached and --revision', file=sys.stderr)
253 sys.exit(1)
254 have_rev = True
255 cmd_argv.extend(opt.revision)
256 cmd_argv.append('--')
257
258 git_failed, bad_rev, have_match = self.ExecuteInParallel(
259 opt.jobs,
260 functools.partial(self._ExecuteOne, cmd_argv),
261 projects,
262 callback=functools.partial(self._ProcessResults, full_name, have_rev),
263 output=out,
264 ordered=True)
265
248 if git_failed: 266 if git_failed:
249 sys.exit(1) 267 sys.exit(1)
250 elif have_match: 268 elif have_match:
diff --git a/subcmds/help.py b/subcmds/help.py
index 78930502..1a60ef45 100644
--- a/subcmds/help.py
+++ b/subcmds/help.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,17 +12,19 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function
18import re 15import re
19import sys 16import sys
20from formatter import AbstractFormatter, DumbWriter 17import textwrap
21 18
19from subcmds import all_commands
22from color import Coloring 20from color import Coloring
23from command import PagedCommand, MirrorSafeCommand, GitcAvailableCommand, GitcClientCommand 21from command import PagedCommand, MirrorSafeCommand, GitcAvailableCommand, GitcClientCommand
24import gitc_utils 22import gitc_utils
23from wrapper import Wrapper
24
25 25
26class Help(PagedCommand, MirrorSafeCommand): 26class Help(PagedCommand, MirrorSafeCommand):
27 common = False 27 COMMON = False
28 helpSummary = "Display detailed help on a command" 28 helpSummary = "Display detailed help on a command"
29 helpUsage = """ 29 helpUsage = """
30%prog [--all|command] 30%prog [--all|command]
@@ -41,7 +41,7 @@ Displays detailed usage information about a command.
41 fmt = ' %%-%ds %%s' % maxlen 41 fmt = ' %%-%ds %%s' % maxlen
42 42
43 for name in commandNames: 43 for name in commandNames:
44 command = self.commands[name] 44 command = all_commands[name]()
45 try: 45 try:
46 summary = command.helpSummary.strip() 46 summary = command.helpSummary.strip()
47 except AttributeError: 47 except AttributeError:
@@ -50,20 +50,27 @@ Displays detailed usage information about a command.
50 50
51 def _PrintAllCommands(self): 51 def _PrintAllCommands(self):
52 print('usage: repo COMMAND [ARGS]') 52 print('usage: repo COMMAND [ARGS]')
53 self.PrintAllCommandsBody()
54
55 def PrintAllCommandsBody(self):
53 print('The complete list of recognized repo commands are:') 56 print('The complete list of recognized repo commands are:')
54 commandNames = list(sorted(self.commands)) 57 commandNames = list(sorted(all_commands))
55 self._PrintCommands(commandNames) 58 self._PrintCommands(commandNames)
56 print("See 'repo help <command>' for more information on a " 59 print("See 'repo help <command>' for more information on a "
57 'specific command.') 60 'specific command.')
61 print('Bug reports:', Wrapper().BUG_URL)
58 62
59 def _PrintCommonCommands(self): 63 def _PrintCommonCommands(self):
60 print('usage: repo COMMAND [ARGS]') 64 print('usage: repo COMMAND [ARGS]')
65 self.PrintCommonCommandsBody()
66
67 def PrintCommonCommandsBody(self):
61 print('The most commonly used repo commands are:') 68 print('The most commonly used repo commands are:')
62 69
63 def gitc_supported(cmd): 70 def gitc_supported(cmd):
64 if not isinstance(cmd, GitcAvailableCommand) and not isinstance(cmd, GitcClientCommand): 71 if not isinstance(cmd, GitcAvailableCommand) and not isinstance(cmd, GitcClientCommand):
65 return True 72 return True
66 if self.manifest.isGitcClient: 73 if self.client.isGitcClient:
67 return True 74 return True
68 if isinstance(cmd, GitcClientCommand): 75 if isinstance(cmd, GitcClientCommand):
69 return False 76 return False
@@ -72,21 +79,21 @@ Displays detailed usage information about a command.
72 return False 79 return False
73 80
74 commandNames = list(sorted([name 81 commandNames = list(sorted([name
75 for name, command in self.commands.items() 82 for name, command in all_commands.items()
76 if command.common and gitc_supported(command)])) 83 if command.COMMON and gitc_supported(command)]))
77 self._PrintCommands(commandNames) 84 self._PrintCommands(commandNames)
78 85
79 print( 86 print(
80"See 'repo help <command>' for more information on a specific command.\n" 87 "See 'repo help <command>' for more information on a specific command.\n"
81"See 'repo help --all' for a complete list of recognized commands.") 88 "See 'repo help --all' for a complete list of recognized commands.")
89 print('Bug reports:', Wrapper().BUG_URL)
82 90
83 def _PrintCommandHelp(self, cmd, header_prefix=''): 91 def _PrintCommandHelp(self, cmd, header_prefix=''):
84 class _Out(Coloring): 92 class _Out(Coloring):
85 def __init__(self, gc): 93 def __init__(self, gc):
86 Coloring.__init__(self, gc, 'help') 94 Coloring.__init__(self, gc, 'help')
87 self.heading = self.printer('heading', attr='bold') 95 self.heading = self.printer('heading', attr='bold')
88 96 self._first = True
89 self.wrap = AbstractFormatter(DumbWriter())
90 97
91 def _PrintSection(self, heading, bodyAttr): 98 def _PrintSection(self, heading, bodyAttr):
92 try: 99 try:
@@ -96,7 +103,9 @@ Displays detailed usage information about a command.
96 if body == '' or body is None: 103 if body == '' or body is None:
97 return 104 return
98 105
99 self.nl() 106 if not self._first:
107 self.nl()
108 self._first = False
100 109
101 self.heading('%s%s', header_prefix, heading) 110 self.heading('%s%s', header_prefix, heading)
102 self.nl() 111 self.nl()
@@ -106,7 +115,8 @@ Displays detailed usage information about a command.
106 body = body.strip() 115 body = body.strip()
107 body = body.replace('%prog', me) 116 body = body.replace('%prog', me)
108 117
109 asciidoc_hdr = re.compile(r'^\n?#+ (.+)$') 118 # Extract the title, but skip any trailing {#anchors}.
119 asciidoc_hdr = re.compile(r'^\n?#+ ([^{]+)(\{#.+\})?$')
110 for para in body.split("\n\n"): 120 for para in body.split("\n\n"):
111 if para.startswith(' '): 121 if para.startswith(' '):
112 self.write('%s', para) 122 self.write('%s', para)
@@ -121,19 +131,21 @@ Displays detailed usage information about a command.
121 self.nl() 131 self.nl()
122 continue 132 continue
123 133
124 self.wrap.add_flowing_data(para) 134 lines = textwrap.wrap(para.replace(' ', ' '), width=80,
125 self.wrap.end_paragraph(1) 135 break_long_words=False, break_on_hyphens=False)
126 self.wrap.end_paragraph(0) 136 for line in lines:
137 self.write('%s', line)
138 self.nl()
139 self.nl()
127 140
128 out = _Out(self.manifest.globalConfig) 141 out = _Out(self.client.globalConfig)
129 out._PrintSection('Summary', 'helpSummary') 142 out._PrintSection('Summary', 'helpSummary')
130 cmd.OptionParser.print_help() 143 cmd.OptionParser.print_help()
131 out._PrintSection('Description', 'helpDescription') 144 out._PrintSection('Description', 'helpDescription')
132 145
133 def _PrintAllCommandHelp(self): 146 def _PrintAllCommandHelp(self):
134 for name in sorted(self.commands): 147 for name in sorted(all_commands):
135 cmd = self.commands[name] 148 cmd = all_commands[name](manifest=self.manifest)
136 cmd.manifest = self.manifest
137 self._PrintCommandHelp(cmd, header_prefix='[%s] ' % (name,)) 149 self._PrintCommandHelp(cmd, header_prefix='[%s] ' % (name,))
138 150
139 def _Options(self, p): 151 def _Options(self, p):
@@ -157,12 +169,11 @@ Displays detailed usage information about a command.
157 name = args[0] 169 name = args[0]
158 170
159 try: 171 try:
160 cmd = self.commands[name] 172 cmd = all_commands[name](manifest=self.manifest)
161 except KeyError: 173 except KeyError:
162 print("repo: '%s' is not a repo command." % name, file=sys.stderr) 174 print("repo: '%s' is not a repo command." % name, file=sys.stderr)
163 sys.exit(1) 175 sys.exit(1)
164 176
165 cmd.manifest = self.manifest
166 self._PrintCommandHelp(cmd) 177 self._PrintCommandHelp(cmd)
167 178
168 else: 179 else:
diff --git a/subcmds/info.py b/subcmds/info.py
index d62e1e64..6c1246ef 100644
--- a/subcmds/info.py
+++ b/subcmds/info.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2012 The Android Open Source Project 1# Copyright (C) 2012 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,18 +12,22 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
15import optparse
16
17from command import PagedCommand 17from command import PagedCommand
18from color import Coloring 18from color import Coloring
19from git_refs import R_M 19from git_refs import R_M, R_HEADS
20
20 21
21class _Coloring(Coloring): 22class _Coloring(Coloring):
22 def __init__(self, config): 23 def __init__(self, config):
23 Coloring.__init__(self, config, "status") 24 Coloring.__init__(self, config, "status")
24 25
26
25class Info(PagedCommand): 27class Info(PagedCommand):
26 common = True 28 COMMON = True
27 helpSummary = "Get info on the manifest branch, current branch or unmerged branches" 29 helpSummary = "Get info on the manifest branch, current branch or unmerged branches"
28 helpUsage = "%prog [-dl] [-o [-b]] [<project>...]" 30 helpUsage = "%prog [-dl] [-o [-c]] [<project>...]"
29 31
30 def _Options(self, p): 32 def _Options(self, p):
31 p.add_option('-d', '--diff', 33 p.add_option('-d', '--diff',
@@ -34,22 +36,28 @@ class Info(PagedCommand):
34 p.add_option('-o', '--overview', 36 p.add_option('-o', '--overview',
35 dest='overview', action='store_true', 37 dest='overview', action='store_true',
36 help='show overview of all local commits') 38 help='show overview of all local commits')
37 p.add_option('-b', '--current-branch', 39 p.add_option('-c', '--current-branch',
38 dest="current_branch", action="store_true", 40 dest="current_branch", action="store_true",
39 help="consider only checked out branches") 41 help="consider only checked out branches")
42 p.add_option('--no-current-branch',
43 dest='current_branch', action='store_false',
44 help='consider all local branches')
45 # Turn this into a warning & remove this someday.
46 p.add_option('-b',
47 dest='current_branch', action='store_true',
48 help=optparse.SUPPRESS_HELP)
40 p.add_option('-l', '--local-only', 49 p.add_option('-l', '--local-only',
41 dest="local", action="store_true", 50 dest="local", action="store_true",
42 help="Disable all remote operations") 51 help="disable all remote operations")
43
44 52
45 def Execute(self, opt, args): 53 def Execute(self, opt, args):
46 self.out = _Coloring(self.manifest.globalConfig) 54 self.out = _Coloring(self.client.globalConfig)
47 self.heading = self.out.printer('heading', attr = 'bold') 55 self.heading = self.out.printer('heading', attr='bold')
48 self.headtext = self.out.nofmt_printer('headtext', fg = 'yellow') 56 self.headtext = self.out.nofmt_printer('headtext', fg='yellow')
49 self.redtext = self.out.printer('redtext', fg = 'red') 57 self.redtext = self.out.printer('redtext', fg='red')
50 self.sha = self.out.printer("sha", fg = 'yellow') 58 self.sha = self.out.printer("sha", fg='yellow')
51 self.text = self.out.nofmt_printer('text') 59 self.text = self.out.nofmt_printer('text')
52 self.dimtext = self.out.printer('dimtext', attr = 'dim') 60 self.dimtext = self.out.printer('dimtext', attr='dim')
53 61
54 self.opt = opt 62 self.opt = opt
55 63
@@ -122,11 +130,14 @@ class Info(PagedCommand):
122 self.printSeparator() 130 self.printSeparator()
123 131
124 def findRemoteLocalDiff(self, project): 132 def findRemoteLocalDiff(self, project):
125 #Fetch all the latest commits 133 # Fetch all the latest commits.
126 if not self.opt.local: 134 if not self.opt.local:
127 project.Sync_NetworkHalf(quiet=True, current_branch_only=True) 135 project.Sync_NetworkHalf(quiet=True, current_branch_only=True)
128 136
129 logTarget = R_M + self.manifest.manifestProject.config.GetBranch("default").merge 137 branch = self.manifest.manifestProject.config.GetBranch('default').merge
138 if branch.startswith(R_HEADS):
139 branch = branch[len(R_HEADS):]
140 logTarget = R_M + branch
130 141
131 bareTmp = project.bare_git._bare 142 bareTmp = project.bare_git._bare
132 project.bare_git._bare = False 143 project.bare_git._bare = False
@@ -195,16 +206,16 @@ class Info(PagedCommand):
195 commits = branch.commits 206 commits = branch.commits
196 date = branch.date 207 date = branch.date
197 self.text('%s %-33s (%2d commit%s, %s)' % ( 208 self.text('%s %-33s (%2d commit%s, %s)' % (
198 branch.name == project.CurrentBranch and '*' or ' ', 209 branch.name == project.CurrentBranch and '*' or ' ',
199 branch.name, 210 branch.name,
200 len(commits), 211 len(commits),
201 len(commits) != 1 and 's' or '', 212 len(commits) != 1 and 's' or '',
202 date)) 213 date))
203 self.out.nl() 214 self.out.nl()
204 215
205 for commit in commits: 216 for commit in commits:
206 split = commit.split() 217 split = commit.split()
207 self.text('{0:38}{1} '.format('','-')) 218 self.text('{0:38}{1} '.format('', '-'))
208 self.sha(split[0] + " ") 219 self.sha(split[0] + " ")
209 self.text(" ".join(split[1:])) 220 self.text(" ".join(split[1:]))
210 self.out.nl() 221 self.out.nl()
diff --git a/subcmds/init.py b/subcmds/init.py
index 6594a602..9c6b2ad9 100644
--- a/subcmds/init.py
+++ b/subcmds/init.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,34 +12,30 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function
18import os 15import os
19import platform 16import platform
20import re 17import re
18import subprocess
21import sys 19import sys
22 20import urllib.parse
23from pyversion import is_python3
24if is_python3():
25 import urllib.parse
26else:
27 import imp
28 import urlparse
29 urllib = imp.new_module('urllib')
30 urllib.parse = urlparse
31 21
32from color import Coloring 22from color import Coloring
33from command import InteractiveCommand, MirrorSafeCommand 23from command import InteractiveCommand, MirrorSafeCommand
34from error import ManifestParseError 24from error import ManifestParseError
35from project import SyncBuffer 25from project import SyncBuffer
36from git_config import GitConfig 26from git_config import GitConfig
37from git_command import git_require, MIN_GIT_VERSION 27from git_command import git_require, MIN_GIT_VERSION_SOFT, MIN_GIT_VERSION_HARD
28import fetch
29import git_superproject
38import platform_utils 30import platform_utils
31from wrapper import Wrapper
32
39 33
40class Init(InteractiveCommand, MirrorSafeCommand): 34class Init(InteractiveCommand, MirrorSafeCommand):
41 common = True 35 COMMON = True
42 helpSummary = "Initialize repo in the current directory" 36 helpSummary = "Initialize a repo client checkout in the current directory"
43 helpUsage = """ 37 helpUsage = """
44%prog [options] 38%prog [options] [manifest url]
45""" 39"""
46 helpDescription = """ 40 helpDescription = """
47The '%prog' command is run once to install and initialize repo. 41The '%prog' command is run once to install and initialize repo.
@@ -49,13 +43,24 @@ The latest repo source code and manifest collection is downloaded
49from the server and is installed in the .repo/ directory in the 43from the server and is installed in the .repo/ directory in the
50current working directory. 44current working directory.
51 45
46When creating a new checkout, the manifest URL is the only required setting.
47It may be specified using the --manifest-url option, or as the first optional
48argument.
49
52The optional -b argument can be used to select the manifest branch 50The optional -b argument can be used to select the manifest branch
53to checkout and use. If no branch is specified, master is assumed. 51to checkout and use. If no branch is specified, the remote's default
52branch is used. This is equivalent to using -b HEAD.
54 53
55The optional -m argument can be used to specify an alternate manifest 54The optional -m argument can be used to specify an alternate manifest
56to be used. If no manifest is specified, the manifest default.xml 55to be used. If no manifest is specified, the manifest default.xml
57will be used. 56will be used.
58 57
58If the --standalone-manifest argument is set, the manifest will be downloaded
59directly from the specified --manifest-url as a static file (rather than
60setting up a manifest git checkout). With --standalone-manifest, the manifest
61will be fully static and will not be re-downloaded during subsesquent
62`repo init` and `repo sync` calls.
63
59The --reference option can be used to point to a directory that 64The --reference option can be used to point to a directory that
60has the content of a --mirror sync. This will make the working 65has the content of a --mirror sync. This will make the working
61directory use as much data as possible from the local reference 66directory use as much data as possible from the local reference
@@ -81,109 +86,64 @@ manifest, a subsequent `repo sync` (or `repo sync -d`) is necessary
81to update the working directory files. 86to update the working directory files.
82""" 87"""
83 88
89 def _CommonOptions(self, p):
90 """Disable due to re-use of Wrapper()."""
91
84 def _Options(self, p, gitc_init=False): 92 def _Options(self, p, gitc_init=False):
85 # Logging 93 Wrapper().InitParser(p, gitc_init=gitc_init)
86 g = p.add_option_group('Logging options')
87 g.add_option('-q', '--quiet',
88 dest="quiet", action="store_true", default=False,
89 help="be quiet")
90
91 # Manifest
92 g = p.add_option_group('Manifest options')
93 g.add_option('-u', '--manifest-url',
94 dest='manifest_url',
95 help='manifest repository location', metavar='URL')
96 g.add_option('-b', '--manifest-branch',
97 dest='manifest_branch',
98 help='manifest branch or revision', metavar='REVISION')
99 cbr_opts = ['--current-branch']
100 # The gitc-init subcommand allocates -c itself, but a lot of init users
101 # want -c, so try to satisfy both as best we can.
102 if not gitc_init:
103 cbr_opts += ['-c']
104 g.add_option(*cbr_opts,
105 dest='current_branch_only', action='store_true',
106 help='fetch only current manifest branch from server')
107 g.add_option('-m', '--manifest-name',
108 dest='manifest_name', default='default.xml',
109 help='initial manifest file', metavar='NAME.xml')
110 g.add_option('--mirror',
111 dest='mirror', action='store_true',
112 help='create a replica of the remote repositories '
113 'rather than a client working directory')
114 g.add_option('--reference',
115 dest='reference',
116 help='location of mirror directory', metavar='DIR')
117 g.add_option('--dissociate',
118 dest='dissociate', action='store_true',
119 help='dissociate from reference mirrors after clone')
120 g.add_option('--depth', type='int', default=None,
121 dest='depth',
122 help='create a shallow clone with given depth; see git clone')
123 g.add_option('--partial-clone', action='store_true',
124 dest='partial_clone',
125 help='perform partial clone (https://git-scm.com/'
126 'docs/gitrepository-layout#_code_partialclone_code)')
127 g.add_option('--clone-filter', action='store', default='blob:none',
128 dest='clone_filter',
129 help='filter for use with --partial-clone [default: %default]')
130 g.add_option('--archive',
131 dest='archive', action='store_true',
132 help='checkout an archive instead of a git repository for '
133 'each project. See git archive.')
134 g.add_option('--submodules',
135 dest='submodules', action='store_true',
136 help='sync any submodules associated with the manifest repo')
137 g.add_option('-g', '--groups',
138 dest='groups', default='default',
139 help='restrict manifest projects to ones with specified '
140 'group(s) [default|all|G1,G2,G3|G4,-G5,-G6]',
141 metavar='GROUP')
142 g.add_option('-p', '--platform',
143 dest='platform', default='auto',
144 help='restrict manifest projects to ones with a specified '
145 'platform group [auto|all|none|linux|darwin|...]',
146 metavar='PLATFORM')
147 g.add_option('--no-clone-bundle',
148 dest='no_clone_bundle', action='store_true',
149 help='disable use of /clone.bundle on HTTP/HTTPS')
150 g.add_option('--no-tags',
151 dest='no_tags', action='store_true',
152 help="don't fetch tags in the manifest")
153
154 # Tool
155 g = p.add_option_group('repo Version options')
156 g.add_option('--repo-url',
157 dest='repo_url',
158 help='repo repository location', metavar='URL')
159 g.add_option('--repo-branch',
160 dest='repo_branch',
161 help='repo branch or revision', metavar='REVISION')
162 g.add_option('--no-repo-verify',
163 dest='no_repo_verify', action='store_true',
164 help='do not verify repo source code')
165
166 # Other
167 g = p.add_option_group('Other options')
168 g.add_option('--config-name',
169 dest='config_name', action="store_true", default=False,
170 help='Always prompt for name/e-mail')
171 94
172 def _RegisteredEnvironmentOptions(self): 95 def _RegisteredEnvironmentOptions(self):
173 return {'REPO_MANIFEST_URL': 'manifest_url', 96 return {'REPO_MANIFEST_URL': 'manifest_url',
174 'REPO_MIRROR_LOCATION': 'reference'} 97 'REPO_MIRROR_LOCATION': 'reference'}
175 98
99 def _CloneSuperproject(self, opt):
100 """Clone the superproject based on the superproject's url and branch.
101
102 Args:
103 opt: Program options returned from optparse. See _Options().
104 """
105 superproject = git_superproject.Superproject(self.manifest,
106 self.repodir,
107 self.git_event_log,
108 quiet=opt.quiet)
109 sync_result = superproject.Sync()
110 if not sync_result.success:
111 print('warning: git update of superproject failed, repo sync will not '
112 'use superproject to fetch source; while this error is not fatal, '
113 'and you can continue to run repo sync, please run repo init with '
114 'the --no-use-superproject option to stop seeing this warning',
115 file=sys.stderr)
116 if sync_result.fatal and opt.use_superproject is not None:
117 sys.exit(1)
118
176 def _SyncManifest(self, opt): 119 def _SyncManifest(self, opt):
177 m = self.manifest.manifestProject 120 m = self.manifest.manifestProject
178 is_new = not m.Exists 121 is_new = not m.Exists
179 122
123 # If repo has already been initialized, we take -u with the absence of
124 # --standalone-manifest to mean "transition to a standard repo set up",
125 # which necessitates starting fresh.
126 # If --standalone-manifest is set, we always tear everything down and start
127 # anew.
128 if not is_new:
129 was_standalone_manifest = m.config.GetString('manifest.standalone')
130 if opt.standalone_manifest or (
131 was_standalone_manifest and opt.manifest_url):
132 m.config.ClearCache()
133 if m.gitdir and os.path.exists(m.gitdir):
134 platform_utils.rmtree(m.gitdir)
135 if m.worktree and os.path.exists(m.worktree):
136 platform_utils.rmtree(m.worktree)
137
138 is_new = not m.Exists
180 if is_new: 139 if is_new:
181 if not opt.manifest_url: 140 if not opt.manifest_url:
182 print('fatal: manifest url (-u) is required.', file=sys.stderr) 141 print('fatal: manifest url is required.', file=sys.stderr)
183 sys.exit(1) 142 sys.exit(1)
184 143
185 if not opt.quiet: 144 if not opt.quiet:
186 print('Get %s' % GitConfig.ForUser().UrlInsteadOf(opt.manifest_url), 145 print('Downloading manifest from %s' %
146 (GitConfig.ForUser().UrlInsteadOf(opt.manifest_url),),
187 file=sys.stderr) 147 file=sys.stderr)
188 148
189 # The manifest project object doesn't keep track of the path on the 149 # The manifest project object doesn't keep track of the path on the
@@ -200,30 +160,52 @@ to update the working directory files.
200 160
201 m._InitGitDir(mirror_git=mirrored_manifest_git) 161 m._InitGitDir(mirror_git=mirrored_manifest_git)
202 162
203 if opt.manifest_branch: 163 # If standalone_manifest is set, mark the project as "standalone" -- we'll
204 m.revisionExpr = opt.manifest_branch 164 # still do much of the manifests.git set up, but will avoid actual syncs to
205 else: 165 # a remote.
206 m.revisionExpr = 'refs/heads/master' 166 standalone_manifest = False
207 else: 167 if opt.standalone_manifest:
208 if opt.manifest_branch: 168 standalone_manifest = True
209 m.revisionExpr = opt.manifest_branch 169 elif not opt.manifest_url:
210 else: 170 # If -u is set and --standalone-manifest is not, then we're not in
211 m.PreSync() 171 # standalone mode. Otherwise, use config to infer what we were in the last
172 # init.
173 standalone_manifest = bool(m.config.GetString('manifest.standalone'))
174 m.config.SetString('manifest.standalone', opt.manifest_url)
212 175
213 self._ConfigureDepth(opt) 176 self._ConfigureDepth(opt)
214 177
178 # Set the remote URL before the remote branch as we might need it below.
215 if opt.manifest_url: 179 if opt.manifest_url:
216 r = m.GetRemote(m.remote.name) 180 r = m.GetRemote(m.remote.name)
217 r.url = opt.manifest_url 181 r.url = opt.manifest_url
218 r.ResetFetch() 182 r.ResetFetch()
219 r.Save() 183 r.Save()
220 184
185 if not standalone_manifest:
186 if opt.manifest_branch:
187 if opt.manifest_branch == 'HEAD':
188 opt.manifest_branch = m.ResolveRemoteHead()
189 if opt.manifest_branch is None:
190 print('fatal: unable to resolve HEAD', file=sys.stderr)
191 sys.exit(1)
192 m.revisionExpr = opt.manifest_branch
193 else:
194 if is_new:
195 default_branch = m.ResolveRemoteHead()
196 if default_branch is None:
197 # If the remote doesn't have HEAD configured, default to master.
198 default_branch = 'refs/heads/master'
199 m.revisionExpr = default_branch
200 else:
201 m.PreSync()
202
221 groups = re.split(r'[,\s]+', opt.groups) 203 groups = re.split(r'[,\s]+', opt.groups)
222 all_platforms = ['linux', 'darwin', 'windows'] 204 all_platforms = ['linux', 'darwin', 'windows']
223 platformize = lambda x: 'platform-' + x 205 platformize = lambda x: 'platform-' + x
224 if opt.platform == 'auto': 206 if opt.platform == 'auto':
225 if (not opt.mirror and 207 if (not opt.mirror and
226 not m.config.GetString('repo.mirror') == 'true'): 208 not m.config.GetString('repo.mirror') == 'true'):
227 groups.append(platformize(platform.system().lower())) 209 groups.append(platformize(platform.system().lower()))
228 elif opt.platform == 'all': 210 elif opt.platform == 'all':
229 groups.extend(map(platformize, all_platforms)) 211 groups.extend(map(platformize, all_platforms))
@@ -235,7 +217,7 @@ to update the working directory files.
235 217
236 groups = [x for x in groups if x] 218 groups = [x for x in groups if x]
237 groupstr = ','.join(groups) 219 groupstr = ','.join(groups)
238 if opt.platform == 'auto' and groupstr == 'default,platform-' + platform.system().lower(): 220 if opt.platform == 'auto' and groupstr == self.manifest.GetDefaultGroupsStr():
239 groupstr = None 221 groupstr = None
240 m.config.SetString('manifest.groups', groupstr) 222 m.config.SetString('manifest.groups', groupstr)
241 223
@@ -243,11 +225,25 @@ to update the working directory files.
243 m.config.SetString('repo.reference', opt.reference) 225 m.config.SetString('repo.reference', opt.reference)
244 226
245 if opt.dissociate: 227 if opt.dissociate:
246 m.config.SetString('repo.dissociate', 'true') 228 m.config.SetBoolean('repo.dissociate', opt.dissociate)
229
230 if opt.worktree:
231 if opt.mirror:
232 print('fatal: --mirror and --worktree are incompatible',
233 file=sys.stderr)
234 sys.exit(1)
235 if opt.submodules:
236 print('fatal: --submodules and --worktree are incompatible',
237 file=sys.stderr)
238 sys.exit(1)
239 m.config.SetBoolean('repo.worktree', opt.worktree)
240 if is_new:
241 m.use_git_worktrees = True
242 print('warning: --worktree is experimental!', file=sys.stderr)
247 243
248 if opt.archive: 244 if opt.archive:
249 if is_new: 245 if is_new:
250 m.config.SetString('repo.archive', 'true') 246 m.config.SetBoolean('repo.archive', opt.archive)
251 else: 247 else:
252 print('fatal: --archive is only supported when initializing a new ' 248 print('fatal: --archive is only supported when initializing a new '
253 'workspace.', file=sys.stderr) 249 'workspace.', file=sys.stderr)
@@ -257,7 +253,7 @@ to update the working directory files.
257 253
258 if opt.mirror: 254 if opt.mirror:
259 if is_new: 255 if is_new:
260 m.config.SetString('repo.mirror', 'true') 256 m.config.SetBoolean('repo.mirror', opt.mirror)
261 else: 257 else:
262 print('fatal: --mirror is only supported when initializing a new ' 258 print('fatal: --mirror is only supported when initializing a new '
263 'workspace.', file=sys.stderr) 259 'workspace.', file=sys.stderr)
@@ -265,25 +261,49 @@ to update the working directory files.
265 'in another location.', file=sys.stderr) 261 'in another location.', file=sys.stderr)
266 sys.exit(1) 262 sys.exit(1)
267 263
268 if opt.partial_clone: 264 if opt.partial_clone is not None:
269 if opt.mirror: 265 if opt.mirror:
270 print('fatal: --mirror and --partial-clone are mutually exclusive', 266 print('fatal: --mirror and --partial-clone are mutually exclusive',
271 file=sys.stderr) 267 file=sys.stderr)
272 sys.exit(1) 268 sys.exit(1)
273 m.config.SetString('repo.partialclone', 'true') 269 m.config.SetBoolean('repo.partialclone', opt.partial_clone)
274 if opt.clone_filter: 270 if opt.clone_filter:
275 m.config.SetString('repo.clonefilter', opt.clone_filter) 271 m.config.SetString('repo.clonefilter', opt.clone_filter)
272 elif m.config.GetBoolean('repo.partialclone'):
273 opt.clone_filter = m.config.GetString('repo.clonefilter')
276 else: 274 else:
277 opt.clone_filter = None 275 opt.clone_filter = None
278 276
277 if opt.partial_clone_exclude is not None:
278 m.config.SetString('repo.partialcloneexclude', opt.partial_clone_exclude)
279
280 if opt.clone_bundle is None:
281 opt.clone_bundle = False if opt.partial_clone else True
282 else:
283 m.config.SetBoolean('repo.clonebundle', opt.clone_bundle)
284
279 if opt.submodules: 285 if opt.submodules:
280 m.config.SetString('repo.submodules', 'true') 286 m.config.SetBoolean('repo.submodules', opt.submodules)
287
288 if opt.use_superproject is not None:
289 m.config.SetBoolean('repo.superproject', opt.use_superproject)
290
291 if standalone_manifest:
292 if is_new:
293 manifest_name = 'default.xml'
294 manifest_data = fetch.fetch_file(opt.manifest_url)
295 dest = os.path.join(m.worktree, manifest_name)
296 os.makedirs(os.path.dirname(dest), exist_ok=True)
297 with open(dest, 'wb') as f:
298 f.write(manifest_data)
299 return
281 300
282 if not m.Sync_NetworkHalf(is_new=is_new, quiet=opt.quiet, 301 if not m.Sync_NetworkHalf(is_new=is_new, quiet=opt.quiet, verbose=opt.verbose,
283 clone_bundle=not opt.no_clone_bundle, 302 clone_bundle=opt.clone_bundle,
284 current_branch_only=opt.current_branch_only, 303 current_branch_only=opt.current_branch_only,
285 no_tags=opt.no_tags, submodules=opt.submodules, 304 tags=opt.tags, submodules=opt.submodules,
286 clone_filter=opt.clone_filter): 305 clone_filter=opt.clone_filter,
306 partial_clone_exclude=self.manifest.PartialCloneExclude):
287 r = m.GetRemote(m.remote.name) 307 r = m.GetRemote(m.remote.name)
288 print('fatal: cannot obtain manifest %s' % r.url, file=sys.stderr) 308 print('fatal: cannot obtain manifest %s' % r.url, file=sys.stderr)
289 309
@@ -326,8 +346,8 @@ to update the working directory files.
326 return value 346 return value
327 return a 347 return a
328 348
329 def _ShouldConfigureUser(self): 349 def _ShouldConfigureUser(self, opt):
330 gc = self.manifest.globalConfig 350 gc = self.client.globalConfig
331 mp = self.manifest.manifestProject 351 mp = self.manifest.manifestProject
332 352
333 # If we don't have local settings, get from global. 353 # If we don't have local settings, get from global.
@@ -338,21 +358,24 @@ to update the working directory files.
338 mp.config.SetString('user.name', gc.GetString('user.name')) 358 mp.config.SetString('user.name', gc.GetString('user.name'))
339 mp.config.SetString('user.email', gc.GetString('user.email')) 359 mp.config.SetString('user.email', gc.GetString('user.email'))
340 360
341 print() 361 if not opt.quiet:
342 print('Your identity is: %s <%s>' % (mp.config.GetString('user.name'), 362 print()
343 mp.config.GetString('user.email'))) 363 print('Your identity is: %s <%s>' % (mp.config.GetString('user.name'),
344 print('If you want to change this, please re-run \'repo init\' with --config-name') 364 mp.config.GetString('user.email')))
365 print("If you want to change this, please re-run 'repo init' with --config-name")
345 return False 366 return False
346 367
347 def _ConfigureUser(self): 368 def _ConfigureUser(self, opt):
348 mp = self.manifest.manifestProject 369 mp = self.manifest.manifestProject
349 370
350 while True: 371 while True:
351 print() 372 if not opt.quiet:
352 name = self._Prompt('Your Name', mp.UserName) 373 print()
374 name = self._Prompt('Your Name', mp.UserName)
353 email = self._Prompt('Your Email', mp.UserEmail) 375 email = self._Prompt('Your Email', mp.UserEmail)
354 376
355 print() 377 if not opt.quiet:
378 print()
356 print('Your identity is: %s <%s>' % (name, email)) 379 print('Your identity is: %s <%s>' % (name, email))
357 print('is this correct [y/N]? ', end='') 380 print('is this correct [y/N]? ', end='')
358 # TODO: When we require Python 3, use flush=True w/print above. 381 # TODO: When we require Python 3, use flush=True w/print above.
@@ -373,7 +396,7 @@ to update the working directory files.
373 return False 396 return False
374 397
375 def _ConfigureColor(self): 398 def _ConfigureColor(self):
376 gc = self.manifest.globalConfig 399 gc = self.client.globalConfig
377 if self._HasColorSet(gc): 400 if self._HasColorSet(gc):
378 return 401 return
379 402
@@ -424,15 +447,16 @@ to update the working directory files.
424 # We store the depth in the main manifest project. 447 # We store the depth in the main manifest project.
425 self.manifest.manifestProject.config.SetString('repo.depth', depth) 448 self.manifest.manifestProject.config.SetString('repo.depth', depth)
426 449
427 def _DisplayResult(self): 450 def _DisplayResult(self, opt):
428 if self.manifest.IsMirror: 451 if self.manifest.IsMirror:
429 init_type = 'mirror ' 452 init_type = 'mirror '
430 else: 453 else:
431 init_type = '' 454 init_type = ''
432 455
433 print() 456 if not opt.quiet:
434 print('repo %shas been initialized in %s' 457 print()
435 % (init_type, self.manifest.topdir)) 458 print('repo %shas been initialized in %s' %
459 (init_type, self.manifest.topdir))
436 460
437 current_dir = os.getcwd() 461 current_dir = os.getcwd()
438 if current_dir != self.manifest.topdir: 462 if current_dir != self.manifest.topdir:
@@ -450,15 +474,61 @@ to update the working directory files.
450 if opt.archive and opt.mirror: 474 if opt.archive and opt.mirror:
451 self.OptionParser.error('--mirror and --archive cannot be used together.') 475 self.OptionParser.error('--mirror and --archive cannot be used together.')
452 476
477 if opt.standalone_manifest and (
478 opt.manifest_branch or opt.manifest_name != 'default.xml'):
479 self.OptionParser.error('--manifest-branch and --manifest-name cannot'
480 ' be used with --standalone-manifest.')
481
482 if args:
483 if opt.manifest_url:
484 self.OptionParser.error(
485 '--manifest-url option and URL argument both specified: only use '
486 'one to select the manifest URL.')
487
488 opt.manifest_url = args.pop(0)
489
490 if args:
491 self.OptionParser.error('too many arguments to init')
492
453 def Execute(self, opt, args): 493 def Execute(self, opt, args):
454 git_require(MIN_GIT_VERSION, fail=True) 494 git_require(MIN_GIT_VERSION_HARD, fail=True)
495 if not git_require(MIN_GIT_VERSION_SOFT):
496 print('repo: warning: git-%s+ will soon be required; please upgrade your '
497 'version of git to maintain support.'
498 % ('.'.join(str(x) for x in MIN_GIT_VERSION_SOFT),),
499 file=sys.stderr)
500
501 rp = self.manifest.repoProject
502
503 # Handle new --repo-url requests.
504 if opt.repo_url:
505 remote = rp.GetRemote('origin')
506 remote.url = opt.repo_url
507 remote.Save()
508
509 # Handle new --repo-rev requests.
510 if opt.repo_rev:
511 wrapper = Wrapper()
512 remote_ref, rev = wrapper.check_repo_rev(
513 rp.gitdir, opt.repo_rev, repo_verify=opt.repo_verify, quiet=opt.quiet)
514 branch = rp.GetBranch('default')
515 branch.merge = remote_ref
516 rp.work_git.reset('--hard', rev)
517 branch.Save()
518
519 if opt.worktree:
520 # Older versions of git supported worktree, but had dangerous gc bugs.
521 git_require((2, 15, 0), fail=True, msg='git gc worktree corruption')
455 522
456 self._SyncManifest(opt) 523 self._SyncManifest(opt)
457 self._LinkManifest(opt.manifest_name) 524 self._LinkManifest(opt.manifest_name)
458 525
526 if self.manifest.manifestProject.config.GetBoolean('repo.superproject'):
527 self._CloneSuperproject(opt)
528
459 if os.isatty(0) and os.isatty(1) and not self.manifest.IsMirror: 529 if os.isatty(0) and os.isatty(1) and not self.manifest.IsMirror:
460 if opt.config_name or self._ShouldConfigureUser(): 530 if opt.config_name or self._ShouldConfigureUser(opt):
461 self._ConfigureUser() 531 self._ConfigureUser(opt)
462 self._ConfigureColor() 532 self._ConfigureColor()
463 533
464 self._DisplayResult() 534 self._DisplayResult(opt)
diff --git a/subcmds/list.py b/subcmds/list.py
index 00172f0e..6adf85b7 100644
--- a/subcmds/list.py
+++ b/subcmds/list.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2011 The Android Open Source Project 1# Copyright (C) 2011 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,45 +12,59 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function 15import os
18import sys
19 16
20from command import Command, MirrorSafeCommand 17from command import Command, MirrorSafeCommand
21 18
19
22class List(Command, MirrorSafeCommand): 20class List(Command, MirrorSafeCommand):
23 common = True 21 COMMON = True
24 helpSummary = "List projects and their associated directories" 22 helpSummary = "List projects and their associated directories"
25 helpUsage = """ 23 helpUsage = """
26%prog [-f] [<project>...] 24%prog [-f] [<project>...]
27%prog [-f] -r str1 [str2]..." 25%prog [-f] -r str1 [str2]...
28""" 26"""
29 helpDescription = """ 27 helpDescription = """
30List all projects; pass '.' to list the project for the cwd. 28List all projects; pass '.' to list the project for the cwd.
31 29
30By default, only projects that currently exist in the checkout are shown. If
31you want to list all projects (using the specified filter settings), use the
32--all option. If you want to show all projects regardless of the manifest
33groups, then also pass --groups all.
34
32This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'. 35This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
33""" 36"""
34 37
35 def _Options(self, p): 38 def _Options(self, p):
36 p.add_option('-r', '--regex', 39 p.add_option('-r', '--regex',
37 dest='regex', action='store_true', 40 dest='regex', action='store_true',
38 help="Filter the project list based on regex or wildcard matching of strings") 41 help='filter the project list based on regex or wildcard matching of strings')
39 p.add_option('-g', '--groups', 42 p.add_option('-g', '--groups',
40 dest='groups', 43 dest='groups',
41 help="Filter the project list based on the groups the project is in") 44 help='filter the project list based on the groups the project is in')
42 p.add_option('-f', '--fullpath', 45 p.add_option('-a', '--all',
43 dest='fullpath', action='store_true', 46 action='store_true',
44 help="Display the full work tree path instead of the relative path") 47 help='show projects regardless of checkout state')
45 p.add_option('-n', '--name-only', 48 p.add_option('-n', '--name-only',
46 dest='name_only', action='store_true', 49 dest='name_only', action='store_true',
47 help="Display only the name of the repository") 50 help='display only the name of the repository')
48 p.add_option('-p', '--path-only', 51 p.add_option('-p', '--path-only',
49 dest='path_only', action='store_true', 52 dest='path_only', action='store_true',
50 help="Display only the path of the repository") 53 help='display only the path of the repository')
54 p.add_option('-f', '--fullpath',
55 dest='fullpath', action='store_true',
56 help='display the full work tree path instead of the relative path')
57 p.add_option('--relative-to', metavar='PATH',
58 help='display paths relative to this one (default: top of repo client checkout)')
51 59
52 def ValidateOptions(self, opt, args): 60 def ValidateOptions(self, opt, args):
53 if opt.fullpath and opt.name_only: 61 if opt.fullpath and opt.name_only:
54 self.OptionParser.error('cannot combine -f and -n') 62 self.OptionParser.error('cannot combine -f and -n')
55 63
64 # Resolve any symlinks so the output is stable.
65 if opt.relative_to:
66 opt.relative_to = os.path.realpath(opt.relative_to)
67
56 def Execute(self, opt, args): 68 def Execute(self, opt, args):
57 """List all projects and the associated directories. 69 """List all projects and the associated directories.
58 70
@@ -65,23 +77,26 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
65 args: Positional args. Can be a list of projects to list, or empty. 77 args: Positional args. Can be a list of projects to list, or empty.
66 """ 78 """
67 if not opt.regex: 79 if not opt.regex:
68 projects = self.GetProjects(args, groups=opt.groups) 80 projects = self.GetProjects(args, groups=opt.groups, missing_ok=opt.all)
69 else: 81 else:
70 projects = self.FindProjects(args) 82 projects = self.FindProjects(args)
71 83
72 def _getpath(x): 84 def _getpath(x):
73 if opt.fullpath: 85 if opt.fullpath:
74 return x.worktree 86 return x.worktree
87 if opt.relative_to:
88 return os.path.relpath(x.worktree, opt.relative_to)
75 return x.relpath 89 return x.relpath
76 90
77 lines = [] 91 lines = []
78 for project in projects: 92 for project in projects:
79 if opt.name_only and not opt.path_only: 93 if opt.name_only and not opt.path_only:
80 lines.append("%s" % ( project.name)) 94 lines.append("%s" % (project.name))
81 elif opt.path_only and not opt.name_only: 95 elif opt.path_only and not opt.name_only:
82 lines.append("%s" % (_getpath(project))) 96 lines.append("%s" % (_getpath(project)))
83 else: 97 else:
84 lines.append("%s : %s" % (_getpath(project), project.name)) 98 lines.append("%s : %s" % (_getpath(project), project.name))
85 99
86 lines.sort() 100 if lines:
87 print('\n'.join(lines)) 101 lines.sort()
102 print('\n'.join(lines))
diff --git a/subcmds/manifest.py b/subcmds/manifest.py
index 9c1b3f0c..0fbdeac0 100644
--- a/subcmds/manifest.py
+++ b/subcmds/manifest.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2009 The Android Open Source Project 1# Copyright (C) 2009 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,25 +12,32 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function 15import json
18import os 16import os
19import sys 17import sys
20 18
21from command import PagedCommand 19from command import PagedCommand
22 20
21
23class Manifest(PagedCommand): 22class Manifest(PagedCommand):
24 common = False 23 COMMON = False
25 helpSummary = "Manifest inspection utility" 24 helpSummary = "Manifest inspection utility"
26 helpUsage = """ 25 helpUsage = """
27%prog [-o {-|NAME.xml} [-r]] 26%prog [-o {-|NAME.xml}] [-m MANIFEST.xml] [-r]
28""" 27"""
29 _helpDescription = """ 28 _helpDescription = """
30 29
31With the -o option, exports the current manifest for inspection. 30With the -o option, exports the current manifest for inspection.
32The manifest and (if present) local_manifest.xml are combined 31The manifest and (if present) local_manifests/ are combined
33together to produce a single manifest file. This file can be stored 32together to produce a single manifest file. This file can be stored
34in a Git repository for use during future 'repo init' invocations. 33in a Git repository for use during future 'repo init' invocations.
35 34
35The -r option can be used to generate a manifest file with project
36revisions set to the current commit hash. These are known as
37"revision locked manifests", as they don't follow a particular branch.
38In this case, the 'upstream' attribute is set to the ref we were on
39when the manifest was generated. The 'dest-branch' attribute is set
40to indicate the remote ref to push changes to via 'repo upload'.
36""" 41"""
37 42
38 @property 43 @property
@@ -48,26 +53,63 @@ in a Git repository for use during future 'repo init' invocations.
48 def _Options(self, p): 53 def _Options(self, p):
49 p.add_option('-r', '--revision-as-HEAD', 54 p.add_option('-r', '--revision-as-HEAD',
50 dest='peg_rev', action='store_true', 55 dest='peg_rev', action='store_true',
51 help='Save revisions as current HEAD') 56 help='save revisions as current HEAD')
57 p.add_option('-m', '--manifest-name',
58 help='temporary manifest to use for this sync', metavar='NAME.xml')
52 p.add_option('--suppress-upstream-revision', dest='peg_rev_upstream', 59 p.add_option('--suppress-upstream-revision', dest='peg_rev_upstream',
53 default=True, action='store_false', 60 default=True, action='store_false',
54 help='If in -r mode, do not write the upstream field. ' 61 help='if in -r mode, do not write the upstream field '
55 'Only of use if the branch names for a sha1 manifest are ' 62 '(only of use if the branch names for a sha1 manifest are '
56 'sensitive.') 63 'sensitive)')
64 p.add_option('--suppress-dest-branch', dest='peg_rev_dest_branch',
65 default=True, action='store_false',
66 help='if in -r mode, do not write the dest-branch field '
67 '(only of use if the branch names for a sha1 manifest are '
68 'sensitive)')
69 p.add_option('--json', default=False, action='store_true',
70 help='output manifest in JSON format (experimental)')
71 p.add_option('--pretty', default=False, action='store_true',
72 help='format output for humans to read')
73 p.add_option('--no-local-manifests', default=False, action='store_true',
74 dest='ignore_local_manifests', help='ignore local manifests')
57 p.add_option('-o', '--output-file', 75 p.add_option('-o', '--output-file',
58 dest='output_file', 76 dest='output_file',
59 default='-', 77 default='-',
60 help='File to save the manifest to', 78 help='file to save the manifest to',
61 metavar='-|NAME.xml') 79 metavar='-|NAME.xml')
62 80
63 def _Output(self, opt): 81 def _Output(self, opt):
82 # If alternate manifest is specified, override the manifest file that we're using.
83 if opt.manifest_name:
84 self.manifest.Override(opt.manifest_name, False)
85
64 if opt.output_file == '-': 86 if opt.output_file == '-':
65 fd = sys.stdout 87 fd = sys.stdout
66 else: 88 else:
67 fd = open(opt.output_file, 'w') 89 fd = open(opt.output_file, 'w')
68 self.manifest.Save(fd, 90
69 peg_rev = opt.peg_rev, 91 self.manifest.SetUseLocalManifests(not opt.ignore_local_manifests)
70 peg_rev_upstream = opt.peg_rev_upstream) 92
93 if opt.json:
94 print('warning: --json is experimental!', file=sys.stderr)
95 doc = self.manifest.ToDict(peg_rev=opt.peg_rev,
96 peg_rev_upstream=opt.peg_rev_upstream,
97 peg_rev_dest_branch=opt.peg_rev_dest_branch)
98
99 json_settings = {
100 # JSON style guide says Uunicode characters are fully allowed.
101 'ensure_ascii': False,
102 # We use 2 space indent to match JSON style guide.
103 'indent': 2 if opt.pretty else None,
104 'separators': (',', ': ') if opt.pretty else (',', ':'),
105 'sort_keys': True,
106 }
107 fd.write(json.dumps(doc, **json_settings))
108 else:
109 self.manifest.Save(fd,
110 peg_rev=opt.peg_rev,
111 peg_rev_upstream=opt.peg_rev_upstream,
112 peg_rev_dest_branch=opt.peg_rev_dest_branch)
71 fd.close() 113 fd.close()
72 if opt.output_file != '-': 114 if opt.output_file != '-':
73 print('Saved manifest to %s' % opt.output_file, file=sys.stderr) 115 print('Saved manifest to %s' % opt.output_file, file=sys.stderr)
diff --git a/subcmds/overview.py b/subcmds/overview.py
index 08b58a6c..63f5a79e 100644
--- a/subcmds/overview.py
+++ b/subcmds/overview.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2012 The Android Open Source Project 1# Copyright (C) 2012 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,13 +12,14 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function 15import optparse
16
18from color import Coloring 17from color import Coloring
19from command import PagedCommand 18from command import PagedCommand
20 19
21 20
22class Overview(PagedCommand): 21class Overview(PagedCommand):
23 common = True 22 COMMON = True
24 helpSummary = "Display overview of unmerged project branches" 23 helpSummary = "Display overview of unmerged project branches"
25 helpUsage = """ 24 helpUsage = """
26%prog [--current-branch] [<project>...] 25%prog [--current-branch] [<project>...]
@@ -29,15 +28,22 @@ class Overview(PagedCommand):
29The '%prog' command is used to display an overview of the projects branches, 28The '%prog' command is used to display an overview of the projects branches,
30and list any local commits that have not yet been merged into the project. 29and list any local commits that have not yet been merged into the project.
31 30
32The -b/--current-branch option can be used to restrict the output to only 31The -c/--current-branch option can be used to restrict the output to only
33branches currently checked out in each project. By default, all branches 32branches currently checked out in each project. By default, all branches
34are displayed. 33are displayed.
35""" 34"""
36 35
37 def _Options(self, p): 36 def _Options(self, p):
38 p.add_option('-b', '--current-branch', 37 p.add_option('-c', '--current-branch',
39 dest="current_branch", action="store_true", 38 dest="current_branch", action="store_true",
40 help="Consider only checked out branches") 39 help="consider only checked out branches")
40 p.add_option('--no-current-branch',
41 dest='current_branch', action='store_false',
42 help='consider all local branches')
43 # Turn this into a warning & remove this someday.
44 p.add_option('-b',
45 dest='current_branch', action='store_true',
46 help=optparse.SUPPRESS_HELP)
41 47
42 def Execute(self, opt, args): 48 def Execute(self, opt, args):
43 all_branches = [] 49 all_branches = []
diff --git a/subcmds/prune.py b/subcmds/prune.py
index ff2fba1d..584ee7ed 100644
--- a/subcmds/prune.py
+++ b/subcmds/prune.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,21 +12,38 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function 15import itertools
16
18from color import Coloring 17from color import Coloring
19from command import PagedCommand 18from command import DEFAULT_LOCAL_JOBS, PagedCommand
19
20 20
21class Prune(PagedCommand): 21class Prune(PagedCommand):
22 common = True 22 COMMON = True
23 helpSummary = "Prune (delete) already merged topics" 23 helpSummary = "Prune (delete) already merged topics"
24 helpUsage = """ 24 helpUsage = """
25%prog [<project>...] 25%prog [<project>...]
26""" 26"""
27 PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
28
29 def _ExecuteOne(self, project):
30 """Process one project."""
31 return project.PruneHeads()
27 32
28 def Execute(self, opt, args): 33 def Execute(self, opt, args):
29 all_branches = [] 34 projects = self.GetProjects(args)
30 for project in self.GetProjects(args): 35
31 all_branches.extend(project.PruneHeads()) 36 # NB: Should be able to refactor this module to display summary as results
37 # come back from children.
38 def _ProcessResults(_pool, _output, results):
39 return list(itertools.chain.from_iterable(results))
40
41 all_branches = self.ExecuteInParallel(
42 opt.jobs,
43 self._ExecuteOne,
44 projects,
45 callback=_ProcessResults,
46 ordered=True)
32 47
33 if not all_branches: 48 if not all_branches:
34 return 49 return
diff --git a/subcmds/rebase.py b/subcmds/rebase.py
index dcb8b2a3..7c53eb7a 100644
--- a/subcmds/rebase.py
+++ b/subcmds/rebase.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2010 The Android Open Source Project 1# Copyright (C) 2010 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,7 +12,6 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function
18import sys 15import sys
19 16
20from color import Coloring 17from color import Coloring
@@ -30,7 +27,7 @@ class RebaseColoring(Coloring):
30 27
31 28
32class Rebase(Command): 29class Rebase(Command):
33 common = True 30 COMMON = True
34 helpSummary = "Rebase local branches on upstream branch" 31 helpSummary = "Rebase local branches on upstream branch"
35 helpUsage = """ 32 helpUsage = """
36%prog {[<project>...] | -i <project>...} 33%prog {[<project>...] | -i <project>...}
@@ -42,36 +39,34 @@ branch but need to incorporate new upstream changes "underneath" them.
42""" 39"""
43 40
44 def _Options(self, p): 41 def _Options(self, p):
45 p.add_option('-i', '--interactive', 42 g = p.get_option_group('--quiet')
46 dest="interactive", action="store_true", 43 g.add_option('-i', '--interactive',
47 help="interactive rebase (single project only)") 44 dest="interactive", action="store_true",
45 help="interactive rebase (single project only)")
48 46
49 p.add_option('--fail-fast', 47 p.add_option('--fail-fast',
50 dest='fail_fast', action='store_true', 48 dest='fail_fast', action='store_true',
51 help='Stop rebasing after first error is hit') 49 help='stop rebasing after first error is hit')
52 p.add_option('-f', '--force-rebase', 50 p.add_option('-f', '--force-rebase',
53 dest='force_rebase', action='store_true', 51 dest='force_rebase', action='store_true',
54 help='Pass --force-rebase to git rebase') 52 help='pass --force-rebase to git rebase')
55 p.add_option('--no-ff', 53 p.add_option('--no-ff',
56 dest='no_ff', action='store_true', 54 dest='ff', default=True, action='store_false',
57 help='Pass --no-ff to git rebase') 55 help='pass --no-ff to git rebase')
58 p.add_option('-q', '--quiet',
59 dest='quiet', action='store_true',
60 help='Pass --quiet to git rebase')
61 p.add_option('--autosquash', 56 p.add_option('--autosquash',
62 dest='autosquash', action='store_true', 57 dest='autosquash', action='store_true',
63 help='Pass --autosquash to git rebase') 58 help='pass --autosquash to git rebase')
64 p.add_option('--whitespace', 59 p.add_option('--whitespace',
65 dest='whitespace', action='store', metavar='WS', 60 dest='whitespace', action='store', metavar='WS',
66 help='Pass --whitespace to git rebase') 61 help='pass --whitespace to git rebase')
67 p.add_option('--auto-stash', 62 p.add_option('--auto-stash',
68 dest='auto_stash', action='store_true', 63 dest='auto_stash', action='store_true',
69 help='Stash local modifications before starting') 64 help='stash local modifications before starting')
70 p.add_option('-m', '--onto-manifest', 65 p.add_option('-m', '--onto-manifest',
71 dest='onto_manifest', action='store_true', 66 dest='onto_manifest', action='store_true',
72 help='Rebase onto the manifest version instead of upstream ' 67 help='rebase onto the manifest version instead of upstream '
73 'HEAD. This helps to make sure the local tree stays ' 68 'HEAD (this helps to make sure the local tree stays '
74 'consistent if you previously synced to a manifest.') 69 'consistent if you previously synced to a manifest)')
75 70
76 def Execute(self, opt, args): 71 def Execute(self, opt, args):
77 all_projects = self.GetProjects(args) 72 all_projects = self.GetProjects(args)
@@ -82,7 +77,7 @@ branch but need to incorporate new upstream changes "underneath" them.
82 file=sys.stderr) 77 file=sys.stderr)
83 if len(args) == 1: 78 if len(args) == 1:
84 print('note: project %s is mapped to more than one path' % (args[0],), 79 print('note: project %s is mapped to more than one path' % (args[0],),
85 file=sys.stderr) 80 file=sys.stderr)
86 return 1 81 return 1
87 82
88 # Setup the common git rebase args that we use for all projects. 83 # Setup the common git rebase args that we use for all projects.
@@ -93,7 +88,7 @@ branch but need to incorporate new upstream changes "underneath" them.
93 common_args.append('--quiet') 88 common_args.append('--quiet')
94 if opt.force_rebase: 89 if opt.force_rebase:
95 common_args.append('--force-rebase') 90 common_args.append('--force-rebase')
96 if opt.no_ff: 91 if not opt.ff:
97 common_args.append('--no-ff') 92 common_args.append('--no-ff')
98 if opt.autosquash: 93 if opt.autosquash:
99 common_args.append('--autosquash') 94 common_args.append('--autosquash')
diff --git a/subcmds/selfupdate.py b/subcmds/selfupdate.py
index a8a09b64..282f518e 100644
--- a/subcmds/selfupdate.py
+++ b/subcmds/selfupdate.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2009 The Android Open Source Project 1# Copyright (C) 2009 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,7 +12,6 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function
18from optparse import SUPPRESS_HELP 15from optparse import SUPPRESS_HELP
19import sys 16import sys
20 17
@@ -22,8 +19,9 @@ from command import Command, MirrorSafeCommand
22from subcmds.sync import _PostRepoUpgrade 19from subcmds.sync import _PostRepoUpgrade
23from subcmds.sync import _PostRepoFetch 20from subcmds.sync import _PostRepoFetch
24 21
22
25class Selfupdate(Command, MirrorSafeCommand): 23class Selfupdate(Command, MirrorSafeCommand):
26 common = False 24 COMMON = False
27 helpSummary = "Update repo to the latest version" 25 helpSummary = "Update repo to the latest version"
28 helpUsage = """ 26 helpUsage = """
29%prog 27%prog
@@ -39,7 +37,7 @@ need to be performed by an end-user.
39 def _Options(self, p): 37 def _Options(self, p):
40 g = p.add_option_group('repo Version options') 38 g = p.add_option_group('repo Version options')
41 g.add_option('--no-repo-verify', 39 g.add_option('--no-repo-verify',
42 dest='no_repo_verify', action='store_true', 40 dest='repo_verify', default=True, action='store_false',
43 help='do not verify repo source code') 41 help='do not verify repo source code')
44 g.add_option('--repo-upgraded', 42 g.add_option('--repo-upgraded',
45 dest='repo_upgraded', action='store_true', 43 dest='repo_upgraded', action='store_true',
@@ -59,5 +57,5 @@ need to be performed by an end-user.
59 57
60 rp.bare_git.gc('--auto') 58 rp.bare_git.gc('--auto')
61 _PostRepoFetch(rp, 59 _PostRepoFetch(rp,
62 no_repo_verify = opt.no_repo_verify, 60 repo_verify=opt.repo_verify,
63 verbose = True) 61 verbose=True)
diff --git a/subcmds/smartsync.py b/subcmds/smartsync.py
index 675b9834..d91d59c6 100644
--- a/subcmds/smartsync.py
+++ b/subcmds/smartsync.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2010 The Android Open Source Project 1# Copyright (C) 2010 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,8 +14,9 @@
16 14
17from subcmds.sync import Sync 15from subcmds.sync import Sync
18 16
17
19class Smartsync(Sync): 18class Smartsync(Sync):
20 common = True 19 COMMON = True
21 helpSummary = "Update working tree to the latest known good revision" 20 helpSummary = "Update working tree to the latest known good revision"
22 helpUsage = """ 21 helpUsage = """
23%prog [<project>...] 22%prog [<project>...]
diff --git a/subcmds/stage.py b/subcmds/stage.py
index aeb49513..0389a4ff 100644
--- a/subcmds/stage.py
+++ b/subcmds/stage.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,13 +12,13 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function
18import sys 15import sys
19 16
20from color import Coloring 17from color import Coloring
21from command import InteractiveCommand 18from command import InteractiveCommand
22from git_command import GitCommand 19from git_command import GitCommand
23 20
21
24class _ProjectList(Coloring): 22class _ProjectList(Coloring):
25 def __init__(self, gc): 23 def __init__(self, gc):
26 Coloring.__init__(self, gc, 'interactive') 24 Coloring.__init__(self, gc, 'interactive')
@@ -28,8 +26,9 @@ class _ProjectList(Coloring):
28 self.header = self.printer('header', attr='bold') 26 self.header = self.printer('header', attr='bold')
29 self.help = self.printer('help', fg='red', attr='bold') 27 self.help = self.printer('help', fg='red', attr='bold')
30 28
29
31class Stage(InteractiveCommand): 30class Stage(InteractiveCommand):
32 common = True 31 COMMON = True
33 helpSummary = "Stage file(s) for commit" 32 helpSummary = "Stage file(s) for commit"
34 helpUsage = """ 33 helpUsage = """
35%prog -i [<project>...] 34%prog -i [<project>...]
@@ -39,7 +38,8 @@ The '%prog' command stages files to prepare the next commit.
39""" 38"""
40 39
41 def _Options(self, p): 40 def _Options(self, p):
42 p.add_option('-i', '--interactive', 41 g = p.get_option_group('--quiet')
42 g.add_option('-i', '--interactive',
43 dest='interactive', action='store_true', 43 dest='interactive', action='store_true',
44 help='use interactive staging') 44 help='use interactive staging')
45 45
@@ -105,6 +105,7 @@ The '%prog' command stages files to prepare the next commit.
105 continue 105 continue
106 print('Bye.') 106 print('Bye.')
107 107
108
108def _AddI(project): 109def _AddI(project):
109 p = GitCommand(project, ['add', '--interactive'], bare=False) 110 p = GitCommand(project, ['add', '--interactive'], bare=False)
110 p.Wait() 111 p.Wait()
diff --git a/subcmds/start.py b/subcmds/start.py
index 6ec0b2ce..2addaf2e 100644
--- a/subcmds/start.py
+++ b/subcmds/start.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,19 +12,20 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function 15import functools
18import os 16import os
19import sys 17import sys
20 18
21from command import Command 19from command import Command, DEFAULT_LOCAL_JOBS
22from git_config import IsImmutable 20from git_config import IsImmutable
23from git_command import git 21from git_command import git
24import gitc_utils 22import gitc_utils
25from progress import Progress 23from progress import Progress
26from project import SyncBuffer 24from project import SyncBuffer
27 25
26
28class Start(Command): 27class Start(Command):
29 common = True 28 COMMON = True
30 helpSummary = "Start a new branch for development" 29 helpSummary = "Start a new branch for development"
31 helpUsage = """ 30 helpUsage = """
32%prog <newbranchname> [--all | <project>...] 31%prog <newbranchname> [--all | <project>...]
@@ -35,6 +34,7 @@ class Start(Command):
35'%prog' begins a new branch of development, starting from the 34'%prog' begins a new branch of development, starting from the
36revision specified in the manifest. 35revision specified in the manifest.
37""" 36"""
37 PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
38 38
39 def _Options(self, p): 39 def _Options(self, p):
40 p.add_option('--all', 40 p.add_option('--all',
@@ -42,7 +42,8 @@ revision specified in the manifest.
42 help='begin branch in all projects') 42 help='begin branch in all projects')
43 p.add_option('-r', '--rev', '--revision', dest='revision', 43 p.add_option('-r', '--rev', '--revision', dest='revision',
44 help='point branch at this revision instead of upstream') 44 help='point branch at this revision instead of upstream')
45 p.add_option('--head', dest='revision', action='store_const', const='HEAD', 45 p.add_option('--head', '--HEAD',
46 dest='revision', action='store_const', const='HEAD',
46 help='abbreviation for --rev HEAD') 47 help='abbreviation for --rev HEAD')
47 48
48 def ValidateOptions(self, opt, args): 49 def ValidateOptions(self, opt, args):
@@ -53,6 +54,26 @@ revision specified in the manifest.
53 if not git.check_ref_format('heads/%s' % nb): 54 if not git.check_ref_format('heads/%s' % nb):
54 self.OptionParser.error("'%s' is not a valid name" % nb) 55 self.OptionParser.error("'%s' is not a valid name" % nb)
55 56
57 def _ExecuteOne(self, revision, nb, project):
58 """Start one project."""
59 # If the current revision is immutable, such as a SHA1, a tag or
60 # a change, then we can't push back to it. Substitute with
61 # dest_branch, if defined; or with manifest default revision instead.
62 branch_merge = ''
63 if IsImmutable(project.revisionExpr):
64 if project.dest_branch:
65 branch_merge = project.dest_branch
66 else:
67 branch_merge = self.manifest.default.revisionExpr
68
69 try:
70 ret = project.StartBranch(
71 nb, branch_merge=branch_merge, revision=revision)
72 except Exception as e:
73 print('error: unable to checkout %s: %s' % (project.name, e), file=sys.stderr)
74 ret = False
75 return (ret, project)
76
56 def Execute(self, opt, args): 77 def Execute(self, opt, args):
57 nb = args[0] 78 nb = args[0]
58 err = [] 79 err = []
@@ -60,7 +81,7 @@ revision specified in the manifest.
60 if not opt.all: 81 if not opt.all:
61 projects = args[1:] 82 projects = args[1:]
62 if len(projects) < 1: 83 if len(projects) < 1:
63 projects = ['.',] # start it in the local project by default 84 projects = ['.'] # start it in the local project by default
64 85
65 all_projects = self.GetProjects(projects, 86 all_projects = self.GetProjects(projects,
66 missing_ok=bool(self.gitc_manifest)) 87 missing_ok=bool(self.gitc_manifest))
@@ -84,11 +105,8 @@ revision specified in the manifest.
84 if not os.path.exists(os.getcwd()): 105 if not os.path.exists(os.getcwd()):
85 os.chdir(self.manifest.topdir) 106 os.chdir(self.manifest.topdir)
86 107
87 pm = Progress('Starting %s' % nb, len(all_projects)) 108 pm = Progress('Syncing %s' % nb, len(all_projects), quiet=opt.quiet)
88 for project in all_projects: 109 for project in all_projects:
89 pm.update()
90
91 if self.gitc_manifest:
92 gitc_project = self.gitc_manifest.paths[project.relpath] 110 gitc_project = self.gitc_manifest.paths[project.relpath]
93 # Sync projects that have not been opened. 111 # Sync projects that have not been opened.
94 if not gitc_project.already_synced: 112 if not gitc_project.already_synced:
@@ -101,21 +119,21 @@ revision specified in the manifest.
101 sync_buf = SyncBuffer(self.manifest.manifestProject.config) 119 sync_buf = SyncBuffer(self.manifest.manifestProject.config)
102 project.Sync_LocalHalf(sync_buf) 120 project.Sync_LocalHalf(sync_buf)
103 project.revisionId = gitc_project.old_revision 121 project.revisionId = gitc_project.old_revision
122 pm.update()
123 pm.end()
104 124
105 # If the current revision is immutable, such as a SHA1, a tag or 125 def _ProcessResults(_pool, pm, results):
106 # a change, then we can't push back to it. Substitute with 126 for (result, project) in results:
107 # dest_branch, if defined; or with manifest default revision instead. 127 if not result:
108 branch_merge = '' 128 err.append(project)
109 if IsImmutable(project.revisionExpr): 129 pm.update()
110 if project.dest_branch:
111 branch_merge = project.dest_branch
112 else:
113 branch_merge = self.manifest.default.revisionExpr
114 130
115 if not project.StartBranch( 131 self.ExecuteInParallel(
116 nb, branch_merge=branch_merge, revision=opt.revision): 132 opt.jobs,
117 err.append(project) 133 functools.partial(self._ExecuteOne, opt.revision, nb),
118 pm.end() 134 all_projects,
135 callback=_ProcessResults,
136 output=Progress('Starting %s' % (nb,), len(all_projects), quiet=opt.quiet))
119 137
120 if err: 138 if err:
121 for p in err: 139 for p in err:
diff --git a/subcmds/status.py b/subcmds/status.py
index 63972d72..5b669547 100644
--- a/subcmds/status.py
+++ b/subcmds/status.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,25 +12,19 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function 15import functools
18
19from command import PagedCommand
20
21try:
22 import threading as _threading
23except ImportError:
24 import dummy_threading as _threading
25
26import glob 16import glob
27 17import io
28import itertools
29import os 18import os
30 19
20from command import DEFAULT_LOCAL_JOBS, PagedCommand
21
31from color import Coloring 22from color import Coloring
32import platform_utils 23import platform_utils
33 24
25
34class Status(PagedCommand): 26class Status(PagedCommand):
35 common = True 27 COMMON = True
36 helpSummary = "Show the working tree status" 28 helpSummary = "Show the working tree status"
37 helpUsage = """ 29 helpUsage = """
38%prog [<project>...] 30%prog [<project>...]
@@ -84,36 +76,29 @@ the following meanings:
84 d: deleted ( in index, not in work tree ) 76 d: deleted ( in index, not in work tree )
85 77
86""" 78"""
79 PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
87 80
88 def _Options(self, p): 81 def _Options(self, p):
89 p.add_option('-j', '--jobs',
90 dest='jobs', action='store', type='int', default=2,
91 help="number of projects to check simultaneously")
92 p.add_option('-o', '--orphans', 82 p.add_option('-o', '--orphans',
93 dest='orphans', action='store_true', 83 dest='orphans', action='store_true',
94 help="include objects in working directory outside of repo projects") 84 help="include objects in working directory outside of repo projects")
95 p.add_option('-q', '--quiet', action='store_true',
96 help="only print the name of modified projects")
97 85
98 def _StatusHelper(self, project, clean_counter, sem, quiet): 86 def _StatusHelper(self, quiet, project):
99 """Obtains the status for a specific project. 87 """Obtains the status for a specific project.
100 88
101 Obtains the status for a project, redirecting the output to 89 Obtains the status for a project, redirecting the output to
102 the specified object. It will release the semaphore 90 the specified object.
103 when done.
104 91
105 Args: 92 Args:
93 quiet: Where to output the status.
106 project: Project to get status of. 94 project: Project to get status of.
107 clean_counter: Counter for clean projects. 95
108 sem: Semaphore, will call release() when complete. 96 Returns:
109 output: Where to output the status. 97 The status of the project.
110 """ 98 """
111 try: 99 buf = io.StringIO()
112 state = project.PrintWorkTreeStatus(quiet=quiet) 100 ret = project.PrintWorkTreeStatus(quiet=quiet, output_redir=buf)
113 if state == 'CLEAN': 101 return (ret, buf.getvalue())
114 next(clean_counter)
115 finally:
116 sem.release()
117 102
118 def _FindOrphans(self, dirs, proj_dirs, proj_dirs_parents, outstring): 103 def _FindOrphans(self, dirs, proj_dirs, proj_dirs_parents, outstring):
119 """find 'dirs' that are present in 'proj_dirs_parents' but not in 'proj_dirs'""" 104 """find 'dirs' that are present in 'proj_dirs_parents' but not in 'proj_dirs'"""
@@ -126,34 +111,31 @@ the following meanings:
126 continue 111 continue
127 if item in proj_dirs_parents: 112 if item in proj_dirs_parents:
128 self._FindOrphans(glob.glob('%s/.*' % item) + 113 self._FindOrphans(glob.glob('%s/.*' % item) +
129 glob.glob('%s/*' % item), 114 glob.glob('%s/*' % item),
130 proj_dirs, proj_dirs_parents, outstring) 115 proj_dirs, proj_dirs_parents, outstring)
131 continue 116 continue
132 outstring.append(''.join([status_header, item, '/'])) 117 outstring.append(''.join([status_header, item, '/']))
133 118
134 def Execute(self, opt, args): 119 def Execute(self, opt, args):
135 all_projects = self.GetProjects(args) 120 all_projects = self.GetProjects(args)
136 counter = itertools.count()
137 121
138 if opt.jobs == 1: 122 def _ProcessResults(_pool, _output, results):
139 for project in all_projects: 123 ret = 0
140 state = project.PrintWorkTreeStatus(quiet=opt.quiet) 124 for (state, output) in results:
125 if output:
126 print(output, end='')
141 if state == 'CLEAN': 127 if state == 'CLEAN':
142 next(counter) 128 ret += 1
143 else: 129 return ret
144 sem = _threading.Semaphore(opt.jobs) 130
145 threads = [] 131 counter = self.ExecuteInParallel(
146 for project in all_projects: 132 opt.jobs,
147 sem.acquire() 133 functools.partial(self._StatusHelper, opt.quiet),
148 134 all_projects,
149 t = _threading.Thread(target=self._StatusHelper, 135 callback=_ProcessResults,
150 args=(project, counter, sem, opt.quiet)) 136 ordered=True)
151 threads.append(t) 137
152 t.daemon = True 138 if not opt.quiet and len(all_projects) == counter:
153 t.start()
154 for t in threads:
155 t.join()
156 if not opt.quiet and len(all_projects) == next(counter):
157 print('nothing to commit (working directory clean)') 139 print('nothing to commit (working directory clean)')
158 140
159 if opt.orphans: 141 if opt.orphans:
@@ -170,8 +152,8 @@ the following meanings:
170 class StatusColoring(Coloring): 152 class StatusColoring(Coloring):
171 def __init__(self, config): 153 def __init__(self, config):
172 Coloring.__init__(self, config, 'status') 154 Coloring.__init__(self, config, 'status')
173 self.project = self.printer('header', attr = 'bold') 155 self.project = self.printer('header', attr='bold')
174 self.untracked = self.printer('untracked', fg = 'red') 156 self.untracked = self.printer('untracked', fg='red')
175 157
176 orig_path = os.getcwd() 158 orig_path = os.getcwd()
177 try: 159 try:
@@ -179,11 +161,11 @@ the following meanings:
179 161
180 outstring = [] 162 outstring = []
181 self._FindOrphans(glob.glob('.*') + 163 self._FindOrphans(glob.glob('.*') +
182 glob.glob('*'), 164 glob.glob('*'),
183 proj_dirs, proj_dirs_parents, outstring) 165 proj_dirs, proj_dirs_parents, outstring)
184 166
185 if outstring: 167 if outstring:
186 output = StatusColoring(self.manifest.globalConfig) 168 output = StatusColoring(self.client.globalConfig)
187 output.project('Objects not within a project (orphans)') 169 output.project('Objects not within a project (orphans)')
188 output.nl() 170 output.nl()
189 for entry in outstring: 171 for entry in outstring:
diff --git a/subcmds/sync.py b/subcmds/sync.py
index 2973a16e..3211cbb1 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,37 +12,23 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function 15import errno
16import functools
17import http.cookiejar as cookielib
18import io
18import json 19import json
20import multiprocessing
19import netrc 21import netrc
20from optparse import SUPPRESS_HELP 22from optparse import SUPPRESS_HELP
21import os 23import os
22import re
23import socket 24import socket
24import subprocess
25import sys 25import sys
26import tempfile 26import tempfile
27import time 27import time
28 28import urllib.error
29from pyversion import is_python3 29import urllib.parse
30if is_python3(): 30import urllib.request
31 import http.cookiejar as cookielib 31import xmlrpc.client
32 import urllib.error
33 import urllib.parse
34 import urllib.request
35 import xmlrpc.client
36else:
37 import cookielib
38 import imp
39 import urllib2
40 import urlparse
41 import xmlrpclib
42 urllib = imp.new_module('urllib')
43 urllib.error = urllib2
44 urllib.parse = urlparse
45 urllib.request = urllib2
46 xmlrpc = imp.new_module('xmlrpc')
47 xmlrpc.client = xmlrpclib
48 32
49try: 33try:
50 import threading as _threading 34 import threading as _threading
@@ -53,44 +37,36 @@ except ImportError:
53 37
54try: 38try:
55 import resource 39 import resource
40
56 def _rlimit_nofile(): 41 def _rlimit_nofile():
57 return resource.getrlimit(resource.RLIMIT_NOFILE) 42 return resource.getrlimit(resource.RLIMIT_NOFILE)
58except ImportError: 43except ImportError:
59 def _rlimit_nofile(): 44 def _rlimit_nofile():
60 return (256, 256) 45 return (256, 256)
61 46
62try:
63 import multiprocessing
64except ImportError:
65 multiprocessing = None
66
67import event_log 47import event_log
68from git_command import GIT, git_require 48from git_command import git_require
69from git_config import GetUrlCookieFile 49from git_config import GetUrlCookieFile
70from git_refs import R_HEADS, HEAD 50from git_refs import R_HEADS, HEAD
51import git_superproject
71import gitc_utils 52import gitc_utils
72from project import Project 53from project import Project
73from project import RemoteSpec 54from project import RemoteSpec
74from command import Command, MirrorSafeCommand 55from command import Command, MirrorSafeCommand, WORKER_BATCH_SIZE
75from error import RepoChangedException, GitError, ManifestParseError 56from error import RepoChangedException, GitError, ManifestParseError
76import platform_utils 57import platform_utils
77from project import SyncBuffer 58from project import SyncBuffer
78from progress import Progress 59from progress import Progress
60import ssh
79from wrapper import Wrapper 61from wrapper import Wrapper
80from manifest_xml import GitcManifest 62from manifest_xml import GitcManifest
81 63
82_ONE_DAY_S = 24 * 60 * 60 64_ONE_DAY_S = 24 * 60 * 60
83 65
84class _FetchError(Exception):
85 """Internal error thrown in _FetchHelper() when we don't want stack trace."""
86 pass
87
88class _CheckoutError(Exception):
89 """Internal error thrown in _CheckoutOne() when we don't want stack trace."""
90 66
91class Sync(Command, MirrorSafeCommand): 67class Sync(Command, MirrorSafeCommand):
92 jobs = 1 68 jobs = 1
93 common = True 69 COMMON = True
94 helpSummary = "Update working tree to the latest revision" 70 helpSummary = "Update working tree to the latest revision"
95 helpUsage = """ 71 helpUsage = """
96%prog [<project>...] 72%prog [<project>...]
@@ -133,11 +109,11 @@ if the manifest server specified in the manifest file already includes
133credentials. 109credentials.
134 110
135By default, all projects will be synced. The --fail-fast option can be used 111By default, all projects will be synced. The --fail-fast option can be used
136to halt syncing as soon as possible when the the first project fails to sync. 112to halt syncing as soon as possible when the first project fails to sync.
137 113
138The --force-sync option can be used to overwrite existing git 114The --force-sync option can be used to overwrite existing git
139directories if they have previously been linked to a different 115directories if they have previously been linked to a different
140object direcotry. WARNING: This may cause data to be lost since 116object directory. WARNING: This may cause data to be lost since
141refs may be removed when overwriting. 117refs may be removed when overwriting.
142 118
143The --force-remove-dirty option can be used to remove previously used 119The --force-remove-dirty option can be used to remove previously used
@@ -191,12 +167,21 @@ If the remote SSH daemon is Gerrit Code Review, version 2.0.10 or
191later is required to fix a server side protocol bug. 167later is required to fix a server side protocol bug.
192 168
193""" 169"""
170 PARALLEL_JOBS = 1
171
172 def _CommonOptions(self, p):
173 if self.manifest:
174 try:
175 self.PARALLEL_JOBS = self.manifest.default.sync_j
176 except ManifestParseError:
177 pass
178 super()._CommonOptions(p)
194 179
195 def _Options(self, p, show_smart=True): 180 def _Options(self, p, show_smart=True):
196 try: 181 p.add_option('--jobs-network', default=None, type=int, metavar='JOBS',
197 self.jobs = self.manifest.default.sync_j 182 help='number of network jobs to run in parallel (defaults to --jobs)')
198 except ManifestParseError: 183 p.add_option('--jobs-checkout', default=None, type=int, metavar='JOBS',
199 self.jobs = 1 184 help='number of local checkout jobs to run in parallel (defaults to --jobs)')
200 185
201 p.add_option('-f', '--force-broken', 186 p.add_option('-f', '--force-broken',
202 dest='force_broken', action='store_true', 187 dest='force_broken', action='store_true',
@@ -217,6 +202,10 @@ later is required to fix a server side protocol bug.
217 p.add_option('-l', '--local-only', 202 p.add_option('-l', '--local-only',
218 dest='local_only', action='store_true', 203 dest='local_only', action='store_true',
219 help="only update working tree, don't fetch") 204 help="only update working tree, don't fetch")
205 p.add_option('--no-manifest-update', '--nmu',
206 dest='mp_update', action='store_false', default='true',
207 help='use the existing manifest checkout as-is. '
208 '(do not update to the latest revision)')
220 p.add_option('-n', '--network-only', 209 p.add_option('-n', '--network-only',
221 dest='network_only', action='store_true', 210 dest='network_only', action='store_true',
222 help="fetch only, don't update working tree") 211 help="fetch only, don't update working tree")
@@ -226,17 +215,15 @@ later is required to fix a server side protocol bug.
226 p.add_option('-c', '--current-branch', 215 p.add_option('-c', '--current-branch',
227 dest='current_branch_only', action='store_true', 216 dest='current_branch_only', action='store_true',
228 help='fetch only current branch from server') 217 help='fetch only current branch from server')
229 p.add_option('-q', '--quiet', 218 p.add_option('--no-current-branch',
230 dest='quiet', action='store_true', 219 dest='current_branch_only', action='store_false',
231 help='be more quiet') 220 help='fetch all branches from server')
232 p.add_option('-j', '--jobs',
233 dest='jobs', action='store', type='int',
234 help="projects to fetch simultaneously (default %d)" % self.jobs)
235 p.add_option('-m', '--manifest-name', 221 p.add_option('-m', '--manifest-name',
236 dest='manifest_name', 222 dest='manifest_name',
237 help='temporary manifest to use for this sync', metavar='NAME.xml') 223 help='temporary manifest to use for this sync', metavar='NAME.xml')
238 p.add_option('--no-clone-bundle', 224 p.add_option('--clone-bundle', action='store_true',
239 dest='no_clone_bundle', action='store_true', 225 help='enable use of /clone.bundle on HTTP/HTTPS')
226 p.add_option('--no-clone-bundle', dest='clone_bundle', action='store_false',
240 help='disable use of /clone.bundle on HTTP/HTTPS') 227 help='disable use of /clone.bundle on HTTP/HTTPS')
241 p.add_option('-u', '--manifest-server-username', action='store', 228 p.add_option('-u', '--manifest-server-username', action='store',
242 dest='manifest_server_username', 229 dest='manifest_server_username',
@@ -247,12 +234,23 @@ later is required to fix a server side protocol bug.
247 p.add_option('--fetch-submodules', 234 p.add_option('--fetch-submodules',
248 dest='fetch_submodules', action='store_true', 235 dest='fetch_submodules', action='store_true',
249 help='fetch submodules from server') 236 help='fetch submodules from server')
237 p.add_option('--use-superproject', action='store_true',
238 help='use the manifest superproject to sync projects')
239 p.add_option('--no-use-superproject', action='store_false',
240 dest='use_superproject',
241 help='disable use of manifest superprojects')
242 p.add_option('--tags',
243 action='store_false',
244 help='fetch tags')
250 p.add_option('--no-tags', 245 p.add_option('--no-tags',
251 dest='no_tags', action='store_true', 246 dest='tags', action='store_false',
252 help="don't fetch tags") 247 help="don't fetch tags")
253 p.add_option('--optimized-fetch', 248 p.add_option('--optimized-fetch',
254 dest='optimized_fetch', action='store_true', 249 dest='optimized_fetch', action='store_true',
255 help='only fetch projects fixed to sha1 if revision does not exist locally') 250 help='only fetch projects fixed to sha1 if revision does not exist locally')
251 p.add_option('--retry-fetches',
252 default=0, action='store', type='int',
253 help='number of times to retry fetches on transient errors')
256 p.add_option('--prune', dest='prune', action='store_true', 254 p.add_option('--prune', dest='prune', action='store_true',
257 help='delete refs that no longer exist on the remote') 255 help='delete refs that no longer exist on the remote')
258 if show_smart: 256 if show_smart:
@@ -265,345 +263,400 @@ later is required to fix a server side protocol bug.
265 263
266 g = p.add_option_group('repo Version options') 264 g = p.add_option_group('repo Version options')
267 g.add_option('--no-repo-verify', 265 g.add_option('--no-repo-verify',
268 dest='no_repo_verify', action='store_true', 266 dest='repo_verify', default=True, action='store_false',
269 help='do not verify repo source code') 267 help='do not verify repo source code')
270 g.add_option('--repo-upgraded', 268 g.add_option('--repo-upgraded',
271 dest='repo_upgraded', action='store_true', 269 dest='repo_upgraded', action='store_true',
272 help=SUPPRESS_HELP) 270 help=SUPPRESS_HELP)
273 271
274 def _FetchProjectList(self, opt, projects, sem, *args, **kwargs): 272 def _GetBranch(self):
275 """Main function of the fetch threads. 273 """Returns the branch name for getting the approved manifest."""
274 p = self.manifest.manifestProject
275 b = p.GetBranch(p.CurrentBranch)
276 branch = b.merge
277 if branch.startswith(R_HEADS):
278 branch = branch[len(R_HEADS):]
279 return branch
280
281 def _GetCurrentBranchOnly(self, opt):
282 """Returns True if current-branch or use-superproject options are enabled."""
283 return opt.current_branch_only or git_superproject.UseSuperproject(opt, self.manifest)
284
285 def _UpdateProjectsRevisionId(self, opt, args, load_local_manifests, superproject_logging_data):
286 """Update revisionId of every project with the SHA from superproject.
287
288 This function updates each project's revisionId with SHA from superproject.
289 It writes the updated manifest into a file and reloads the manifest from it.
290
291 Args:
292 opt: Program options returned from optparse. See _Options().
293 args: Arguments to pass to GetProjects. See the GetProjects
294 docstring for details.
295 load_local_manifests: Whether to load local manifests.
296 superproject_logging_data: A dictionary of superproject data that is to be logged.
297
298 Returns:
299 Returns path to the overriding manifest file instead of None.
300 """
301 print_messages = git_superproject.PrintMessages(opt, self.manifest)
302 superproject = git_superproject.Superproject(self.manifest,
303 self.repodir,
304 self.git_event_log,
305 quiet=opt.quiet,
306 print_messages=print_messages)
307 if opt.local_only:
308 manifest_path = superproject.manifest_path
309 if manifest_path:
310 self._ReloadManifest(manifest_path, load_local_manifests)
311 return manifest_path
312
313 all_projects = self.GetProjects(args,
314 missing_ok=True,
315 submodules_ok=opt.fetch_submodules)
316 update_result = superproject.UpdateProjectsRevisionId(all_projects)
317 manifest_path = update_result.manifest_path
318 superproject_logging_data['updatedrevisionid'] = bool(manifest_path)
319 if manifest_path:
320 self._ReloadManifest(manifest_path, load_local_manifests)
321 else:
322 if print_messages:
323 print('warning: Update of revisionId from superproject has failed, '
324 'repo sync will not use superproject to fetch the source. ',
325 'Please resync with the --no-use-superproject option to avoid this repo warning.',
326 file=sys.stderr)
327 if update_result.fatal and opt.use_superproject is not None:
328 sys.exit(1)
329 return manifest_path
330
331 def _FetchProjectList(self, opt, projects):
332 """Main function of the fetch worker.
333
334 The projects we're given share the same underlying git object store, so we
335 have to fetch them in serial.
276 336
277 Delegates most of the work to _FetchHelper. 337 Delegates most of the work to _FetchHelper.
278 338
279 Args: 339 Args:
280 opt: Program options returned from optparse. See _Options(). 340 opt: Program options returned from optparse. See _Options().
281 projects: Projects to fetch. 341 projects: Projects to fetch.
282 sem: We'll release() this semaphore when we exit so that another thread
283 can be started up.
284 *args, **kwargs: Remaining arguments to pass to _FetchHelper. See the
285 _FetchHelper docstring for details.
286 """ 342 """
287 try: 343 return [self._FetchOne(opt, x) for x in projects]
288 for project in projects:
289 success = self._FetchHelper(opt, project, *args, **kwargs)
290 if not success and opt.fail_fast:
291 break
292 finally:
293 sem.release()
294 344
295 def _FetchHelper(self, opt, project, lock, fetched, pm, err_event, 345 def _FetchOne(self, opt, project):
296 clone_filter):
297 """Fetch git objects for a single project. 346 """Fetch git objects for a single project.
298 347
299 Args: 348 Args:
300 opt: Program options returned from optparse. See _Options(). 349 opt: Program options returned from optparse. See _Options().
301 project: Project object for the project to fetch. 350 project: Project object for the project to fetch.
302 lock: Lock for accessing objects that are shared amongst multiple
303 _FetchHelper() threads.
304 fetched: set object that we will add project.gitdir to when we're done
305 (with our lock held).
306 pm: Instance of a Project object. We will call pm.update() (with our
307 lock held).
308 err_event: We'll set this event in the case of an error (after printing
309 out info about the error).
310 clone_filter: Filter for use in a partial clone.
311 351
312 Returns: 352 Returns:
313 Whether the fetch was successful. 353 Whether the fetch was successful.
314 """ 354 """
315 # We'll set to true once we've locked the lock.
316 did_lock = False
317
318 # Encapsulate everything in a try/except/finally so that:
319 # - We always set err_event in the case of an exception.
320 # - We always make sure we unlock the lock if we locked it.
321 start = time.time() 355 start = time.time()
322 success = False 356 success = False
357 buf = io.StringIO()
323 try: 358 try:
324 try: 359 success = project.Sync_NetworkHalf(
325 success = project.Sync_NetworkHalf(
326 quiet=opt.quiet, 360 quiet=opt.quiet,
327 current_branch_only=opt.current_branch_only, 361 verbose=opt.verbose,
362 output_redir=buf,
363 current_branch_only=self._GetCurrentBranchOnly(opt),
328 force_sync=opt.force_sync, 364 force_sync=opt.force_sync,
329 clone_bundle=not opt.no_clone_bundle, 365 clone_bundle=opt.clone_bundle,
330 no_tags=opt.no_tags, archive=self.manifest.IsArchive, 366 tags=opt.tags, archive=self.manifest.IsArchive,
331 optimized_fetch=opt.optimized_fetch, 367 optimized_fetch=opt.optimized_fetch,
368 retry_fetches=opt.retry_fetches,
332 prune=opt.prune, 369 prune=opt.prune,
333 clone_filter=clone_filter) 370 ssh_proxy=self.ssh_proxy,
334 self._fetch_times.Set(project, time.time() - start) 371 clone_filter=self.manifest.CloneFilter,
372 partial_clone_exclude=self.manifest.PartialCloneExclude)
335 373
336 # Lock around all the rest of the code, since printing, updating a set 374 output = buf.getvalue()
337 # and Progress.update() are not thread safe. 375 if (opt.verbose or not success) and output:
338 lock.acquire() 376 print('\n' + output.rstrip())
339 did_lock = True
340 377
341 if not success: 378 if not success:
342 err_event.set() 379 print('error: Cannot fetch %s from %s'
343 print('error: Cannot fetch %s from %s' 380 % (project.name, project.remote.url),
344 % (project.name, project.remote.url), 381 file=sys.stderr)
345 file=sys.stderr) 382 except GitError as e:
346 if opt.fail_fast: 383 print('error.GitError: Cannot fetch %s' % str(e), file=sys.stderr)
347 raise _FetchError() 384 except Exception as e:
348 385 print('error: Cannot fetch %s (%s: %s)'
349 fetched.add(project.gitdir)
350 pm.update(msg=project.name)
351 except _FetchError:
352 pass
353 except Exception as e:
354 print('error: Cannot fetch %s (%s: %s)' \
355 % (project.name, type(e).__name__, str(e)), file=sys.stderr) 386 % (project.name, type(e).__name__, str(e)), file=sys.stderr)
356 err_event.set() 387 raise
357 raise 388
358 finally: 389 finish = time.time()
359 if did_lock: 390 return (success, project, start, finish)
360 lock.release()
361 finish = time.time()
362 self.event_log.AddSync(project, event_log.TASK_SYNC_NETWORK,
363 start, finish, success)
364 391
365 return success 392 @classmethod
393 def _FetchInitChild(cls, ssh_proxy):
394 cls.ssh_proxy = ssh_proxy
366 395
367 def _Fetch(self, projects, opt): 396 def _Fetch(self, projects, opt, err_event, ssh_proxy):
397 ret = True
398
399 jobs = opt.jobs_network if opt.jobs_network else self.jobs
368 fetched = set() 400 fetched = set()
369 lock = _threading.Lock() 401 pm = Progress('Fetching', len(projects), delay=False, quiet=opt.quiet)
370 pm = Progress('Fetching projects', len(projects),
371 always_print_percentage=opt.quiet)
372 402
373 objdir_project_map = dict() 403 objdir_project_map = dict()
374 for project in projects: 404 for project in projects:
375 objdir_project_map.setdefault(project.objdir, []).append(project) 405 objdir_project_map.setdefault(project.objdir, []).append(project)
376 406 projects_list = list(objdir_project_map.values())
377 threads = set() 407
378 sem = _threading.Semaphore(self.jobs) 408 def _ProcessResults(results_sets):
379 err_event = _threading.Event() 409 ret = True
380 for project_list in objdir_project_map.values(): 410 for results in results_sets:
381 # Check for any errors before running any more tasks. 411 for (success, project, start, finish) in results:
382 # ...we'll let existing threads finish, though. 412 self._fetch_times.Set(project, finish - start)
383 if err_event.isSet() and opt.fail_fast: 413 self.event_log.AddSync(project, event_log.TASK_SYNC_NETWORK,
384 break 414 start, finish, success)
385 415 # Check for any errors before running any more tasks.
386 sem.acquire() 416 # ...we'll let existing jobs finish, though.
387 kwargs = dict(opt=opt, 417 if not success:
388 projects=project_list, 418 ret = False
389 sem=sem, 419 else:
390 lock=lock, 420 fetched.add(project.gitdir)
391 fetched=fetched, 421 pm.update(msg=project.name)
392 pm=pm, 422 if not ret and opt.fail_fast:
393 err_event=err_event, 423 break
394 clone_filter=self.manifest.CloneFilter) 424 return ret
395 if self.jobs > 1: 425
396 t = _threading.Thread(target = self._FetchProjectList, 426 # We pass the ssh proxy settings via the class. This allows multiprocessing
397 kwargs = kwargs) 427 # to pickle it up when spawning children. We can't pass it as an argument
398 # Ensure that Ctrl-C will not freeze the repo process. 428 # to _FetchProjectList below as multiprocessing is unable to pickle those.
399 t.daemon = True 429 Sync.ssh_proxy = None
400 threads.add(t) 430
401 t.start() 431 # NB: Multiprocessing is heavy, so don't spin it up for one job.
432 if len(projects_list) == 1 or jobs == 1:
433 self._FetchInitChild(ssh_proxy)
434 if not _ProcessResults(self._FetchProjectList(opt, x) for x in projects_list):
435 ret = False
436 else:
437 # Favor throughput over responsiveness when quiet. It seems that imap()
438 # will yield results in batches relative to chunksize, so even as the
439 # children finish a sync, we won't see the result until one child finishes
440 # ~chunksize jobs. When using a large --jobs with large chunksize, this
441 # can be jarring as there will be a large initial delay where repo looks
442 # like it isn't doing anything and sits at 0%, but then suddenly completes
443 # a lot of jobs all at once. Since this code is more network bound, we
444 # can accept a bit more CPU overhead with a smaller chunksize so that the
445 # user sees more immediate & continuous feedback.
446 if opt.quiet:
447 chunksize = WORKER_BATCH_SIZE
402 else: 448 else:
403 self._FetchProjectList(**kwargs) 449 pm.update(inc=0, msg='warming up')
404 450 chunksize = 4
405 for t in threads: 451 with multiprocessing.Pool(
406 t.join() 452 jobs, initializer=self._FetchInitChild, initargs=(ssh_proxy,)) as pool:
407 453 results = pool.imap_unordered(
408 # If we saw an error, exit with code 1 so that other scripts can check. 454 functools.partial(self._FetchProjectList, opt),
409 if err_event.isSet() and opt.fail_fast: 455 projects_list,
410 print('\nerror: Exited sync due to fetch errors', file=sys.stderr) 456 chunksize=chunksize)
411 sys.exit(1) 457 if not _ProcessResults(results):
458 ret = False
459 pool.close()
460
461 # Cleanup the reference now that we're done with it, and we're going to
462 # release any resources it points to. If we don't, later multiprocessing
463 # usage (e.g. checkouts) will try to pickle and then crash.
464 del Sync.ssh_proxy
412 465
413 pm.end() 466 pm.end()
414 self._fetch_times.Save() 467 self._fetch_times.Save()
415 468
416 if not self.manifest.IsArchive: 469 if not self.manifest.IsArchive:
417 self._GCProjects(projects) 470 self._GCProjects(projects, opt, err_event)
418 471
419 return fetched 472 return (ret, fetched)
420 473
421 def _CheckoutWorker(self, opt, sem, project, *args, **kwargs): 474 def _FetchMain(self, opt, args, all_projects, err_event, manifest_name,
422 """Main function of the fetch threads. 475 load_local_manifests, ssh_proxy):
423 476 """The main network fetch loop.
424 Delegates most of the work to _CheckoutOne.
425 477
426 Args: 478 Args:
427 opt: Program options returned from optparse. See _Options(). 479 opt: Program options returned from optparse. See _Options().
428 projects: Projects to fetch. 480 args: Command line args used to filter out projects.
429 sem: We'll release() this semaphore when we exit so that another thread 481 all_projects: List of all projects that should be fetched.
430 can be started up. 482 err_event: Whether an error was hit while processing.
431 *args, **kwargs: Remaining arguments to pass to _CheckoutOne. See the 483 manifest_name: Manifest file to be reloaded.
432 _CheckoutOne docstring for details. 484 load_local_manifests: Whether to load local manifests.
485 ssh_proxy: SSH manager for clients & masters.
486
487 Returns:
488 List of all projects that should be checked out.
433 """ 489 """
434 try: 490 rp = self.manifest.repoProject
435 return self._CheckoutOne(opt, project, *args, **kwargs)
436 finally:
437 sem.release()
438 491
439 def _CheckoutOne(self, opt, project, lock, pm, err_event, err_results): 492 to_fetch = []
493 now = time.time()
494 if _ONE_DAY_S <= (now - rp.LastFetch):
495 to_fetch.append(rp)
496 to_fetch.extend(all_projects)
497 to_fetch.sort(key=self._fetch_times.Get, reverse=True)
498
499 success, fetched = self._Fetch(to_fetch, opt, err_event, ssh_proxy)
500 if not success:
501 err_event.set()
502
503 _PostRepoFetch(rp, opt.repo_verify)
504 if opt.network_only:
505 # bail out now; the rest touches the working tree
506 if err_event.is_set():
507 print('\nerror: Exited sync due to fetch errors.\n', file=sys.stderr)
508 sys.exit(1)
509 return
510
511 # Iteratively fetch missing and/or nested unregistered submodules
512 previously_missing_set = set()
513 while True:
514 self._ReloadManifest(manifest_name, load_local_manifests)
515 all_projects = self.GetProjects(args,
516 missing_ok=True,
517 submodules_ok=opt.fetch_submodules)
518 missing = []
519 for project in all_projects:
520 if project.gitdir not in fetched:
521 missing.append(project)
522 if not missing:
523 break
524 # Stop us from non-stopped fetching actually-missing repos: If set of
525 # missing repos has not been changed from last fetch, we break.
526 missing_set = set(p.name for p in missing)
527 if previously_missing_set == missing_set:
528 break
529 previously_missing_set = missing_set
530 success, new_fetched = self._Fetch(missing, opt, err_event, ssh_proxy)
531 if not success:
532 err_event.set()
533 fetched.update(new_fetched)
534
535 return all_projects
536
537 def _CheckoutOne(self, detach_head, force_sync, project):
440 """Checkout work tree for one project 538 """Checkout work tree for one project
441 539
442 Args: 540 Args:
443 opt: Program options returned from optparse. See _Options(). 541 detach_head: Whether to leave a detached HEAD.
542 force_sync: Force checking out of the repo.
444 project: Project object for the project to checkout. 543 project: Project object for the project to checkout.
445 lock: Lock for accessing objects that are shared amongst multiple
446 _CheckoutWorker() threads.
447 pm: Instance of a Project object. We will call pm.update() (with our
448 lock held).
449 err_event: We'll set this event in the case of an error (after printing
450 out info about the error).
451 err_results: A list of strings, paths to git repos where checkout
452 failed.
453 544
454 Returns: 545 Returns:
455 Whether the fetch was successful. 546 Whether the fetch was successful.
456 """ 547 """
457 # We'll set to true once we've locked the lock.
458 did_lock = False
459
460 # Encapsulate everything in a try/except/finally so that:
461 # - We always set err_event in the case of an exception.
462 # - We always make sure we unlock the lock if we locked it.
463 start = time.time() 548 start = time.time()
464 syncbuf = SyncBuffer(self.manifest.manifestProject.config, 549 syncbuf = SyncBuffer(self.manifest.manifestProject.config,
465 detach_head=opt.detach_head) 550 detach_head=detach_head)
466 success = False 551 success = False
467 try: 552 try:
468 try: 553 project.Sync_LocalHalf(syncbuf, force_sync=force_sync)
469 project.Sync_LocalHalf(syncbuf, force_sync=opt.force_sync) 554 success = syncbuf.Finish()
470 555 except GitError as e:
471 # Lock around all the rest of the code, since printing, updating a set 556 print('error.GitError: Cannot checkout %s: %s' %
472 # and Progress.update() are not thread safe. 557 (project.name, str(e)), file=sys.stderr)
473 lock.acquire() 558 except Exception as e:
474 success = syncbuf.Finish() 559 print('error: Cannot checkout %s: %s: %s' %
475 did_lock = True 560 (project.name, type(e).__name__, str(e)),
476 561 file=sys.stderr)
477 if not success: 562 raise
478 err_event.set()
479 print('error: Cannot checkout %s' % (project.name),
480 file=sys.stderr)
481 raise _CheckoutError()
482
483 pm.update(msg=project.name)
484 except _CheckoutError:
485 pass
486 except Exception as e:
487 print('error: Cannot checkout %s: %s: %s' %
488 (project.name, type(e).__name__, str(e)),
489 file=sys.stderr)
490 err_event.set()
491 raise
492 finally:
493 if did_lock:
494 if not success:
495 err_results.append(project.relpath)
496 lock.release()
497 finish = time.time()
498 self.event_log.AddSync(project, event_log.TASK_SYNC_LOCAL,
499 start, finish, success)
500 563
501 return success 564 if not success:
565 print('error: Cannot checkout %s' % (project.name), file=sys.stderr)
566 finish = time.time()
567 return (success, project, start, finish)
502 568
503 def _Checkout(self, all_projects, opt): 569 def _Checkout(self, all_projects, opt, err_results):
504 """Checkout projects listed in all_projects 570 """Checkout projects listed in all_projects
505 571
506 Args: 572 Args:
507 all_projects: List of all projects that should be checked out. 573 all_projects: List of all projects that should be checked out.
508 opt: Program options returned from optparse. See _Options(). 574 opt: Program options returned from optparse. See _Options().
575 err_results: A list of strings, paths to git repos where checkout failed.
509 """ 576 """
577 # Only checkout projects with worktrees.
578 all_projects = [x for x in all_projects if x.worktree]
579
580 def _ProcessResults(pool, pm, results):
581 ret = True
582 for (success, project, start, finish) in results:
583 self.event_log.AddSync(project, event_log.TASK_SYNC_LOCAL,
584 start, finish, success)
585 # Check for any errors before running any more tasks.
586 # ...we'll let existing jobs finish, though.
587 if not success:
588 ret = False
589 err_results.append(project.relpath)
590 if opt.fail_fast:
591 if pool:
592 pool.close()
593 return ret
594 pm.update(msg=project.name)
595 return ret
510 596
511 # Perform checkouts in multiple threads when we are using partial clone. 597 return self.ExecuteInParallel(
512 # Without partial clone, all needed git objects are already downloaded, 598 opt.jobs_checkout if opt.jobs_checkout else self.jobs,
513 # in this situation it's better to use only one process because the checkout 599 functools.partial(self._CheckoutOne, opt.detach_head, opt.force_sync),
514 # would be mostly disk I/O; with partial clone, the objects are only 600 all_projects,
515 # downloaded when demanded (at checkout time), which is similar to the 601 callback=_ProcessResults,
516 # Sync_NetworkHalf case and parallelism would be helpful. 602 output=Progress('Checking out', len(all_projects), quiet=opt.quiet)) and not err_results
517 if self.manifest.CloneFilter:
518 syncjobs = self.jobs
519 else:
520 syncjobs = 1
521
522 lock = _threading.Lock()
523 pm = Progress('Checking out projects', len(all_projects))
524
525 threads = set()
526 sem = _threading.Semaphore(syncjobs)
527 err_event = _threading.Event()
528 err_results = []
529
530 for project in all_projects:
531 # Check for any errors before running any more tasks.
532 # ...we'll let existing threads finish, though.
533 if err_event.isSet() and opt.fail_fast:
534 break
535
536 sem.acquire()
537 if project.worktree:
538 kwargs = dict(opt=opt,
539 sem=sem,
540 project=project,
541 lock=lock,
542 pm=pm,
543 err_event=err_event,
544 err_results=err_results)
545 if syncjobs > 1:
546 t = _threading.Thread(target=self._CheckoutWorker,
547 kwargs=kwargs)
548 # Ensure that Ctrl-C will not freeze the repo process.
549 t.daemon = True
550 threads.add(t)
551 t.start()
552 else:
553 self._CheckoutWorker(**kwargs)
554
555 for t in threads:
556 t.join()
557 603
558 pm.end() 604 def _GCProjects(self, projects, opt, err_event):
559 # If we saw an error, exit with code 1 so that other scripts can check. 605 pm = Progress('Garbage collecting', len(projects), delay=False, quiet=opt.quiet)
560 if err_event.isSet(): 606 pm.update(inc=0, msg='prescan')
561 print('\nerror: Exited sync due to checkout errors', file=sys.stderr)
562 if err_results:
563 print('Failing repos:\n%s' % '\n'.join(err_results),
564 file=sys.stderr)
565 sys.exit(1)
566 607
567 def _GCProjects(self, projects):
568 gc_gitdirs = {} 608 gc_gitdirs = {}
569 for project in projects: 609 for project in projects:
570 if len(project.manifest.GetProjectsWithName(project.name)) > 1: 610 # Make sure pruning never kicks in with shared projects.
571 print('Shared project %s found, disabling pruning.' % project.name) 611 if (not project.use_git_worktrees and
572 project.bare_git.config('--replace-all', 'gc.pruneExpire', 'never') 612 len(project.manifest.GetProjectsWithName(project.name)) > 1):
613 if not opt.quiet:
614 print('\r%s: Shared project %s found, disabling pruning.' %
615 (project.relpath, project.name))
616 if git_require((2, 7, 0)):
617 project.EnableRepositoryExtension('preciousObjects')
618 else:
619 # This isn't perfect, but it's the best we can do with old git.
620 print('\r%s: WARNING: shared projects are unreliable when using old '
621 'versions of git; please upgrade to git-2.7.0+.'
622 % (project.relpath,),
623 file=sys.stderr)
624 project.config.SetString('gc.pruneExpire', 'never')
573 gc_gitdirs[project.gitdir] = project.bare_git 625 gc_gitdirs[project.gitdir] = project.bare_git
574 626
575 has_dash_c = git_require((1, 7, 2)) 627 pm.update(inc=len(projects) - len(gc_gitdirs), msg='warming up')
576 if multiprocessing and has_dash_c: 628
577 cpu_count = multiprocessing.cpu_count() 629 cpu_count = os.cpu_count()
578 else:
579 cpu_count = 1
580 jobs = min(self.jobs, cpu_count) 630 jobs = min(self.jobs, cpu_count)
581 631
582 if jobs < 2: 632 if jobs < 2:
583 for bare_git in gc_gitdirs.values(): 633 for bare_git in gc_gitdirs.values():
634 pm.update(msg=bare_git._project.name)
584 bare_git.gc('--auto') 635 bare_git.gc('--auto')
636 pm.end()
585 return 637 return
586 638
587 config = {'pack.threads': cpu_count // jobs if cpu_count > jobs else 1} 639 config = {'pack.threads': cpu_count // jobs if cpu_count > jobs else 1}
588 640
589 threads = set() 641 threads = set()
590 sem = _threading.Semaphore(jobs) 642 sem = _threading.Semaphore(jobs)
591 err_event = _threading.Event()
592 643
593 def GC(bare_git): 644 def GC(bare_git):
645 pm.start(bare_git._project.name)
594 try: 646 try:
595 try: 647 try:
596 bare_git.gc('--auto', config=config) 648 bare_git.gc('--auto', config=config)
597 except GitError: 649 except GitError:
598 err_event.set() 650 err_event.set()
599 except: 651 except Exception:
600 err_event.set() 652 err_event.set()
601 raise 653 raise
602 finally: 654 finally:
655 pm.finish(bare_git._project.name)
603 sem.release() 656 sem.release()
604 657
605 for bare_git in gc_gitdirs.values(): 658 for bare_git in gc_gitdirs.values():
606 if err_event.isSet(): 659 if err_event.is_set() and opt.fail_fast:
607 break 660 break
608 sem.acquire() 661 sem.acquire()
609 t = _threading.Thread(target=GC, args=(bare_git,)) 662 t = _threading.Thread(target=GC, args=(bare_git,))
@@ -613,84 +666,30 @@ later is required to fix a server side protocol bug.
613 666
614 for t in threads: 667 for t in threads:
615 t.join() 668 t.join()
669 pm.end()
616 670
617 if err_event.isSet(): 671 def _ReloadManifest(self, manifest_name=None, load_local_manifests=True):
618 print('\nerror: Exited sync due to gc errors', file=sys.stderr) 672 """Reload the manfiest from the file specified by the |manifest_name|.
619 sys.exit(1) 673
674 It unloads the manifest if |manifest_name| is None.
620 675
621 def _ReloadManifest(self, manifest_name=None): 676 Args:
677 manifest_name: Manifest file to be reloaded.
678 load_local_manifests: Whether to load local manifests.
679 """
622 if manifest_name: 680 if manifest_name:
623 # Override calls _Unload already 681 # Override calls _Unload already
624 self.manifest.Override(manifest_name) 682 self.manifest.Override(manifest_name, load_local_manifests=load_local_manifests)
625 else: 683 else:
626 self.manifest._Unload() 684 self.manifest._Unload()
627 685
628 def _DeleteProject(self, path):
629 print('Deleting obsolete path %s' % path, file=sys.stderr)
630
631 # Delete the .git directory first, so we're less likely to have a partially
632 # working git repository around. There shouldn't be any git projects here,
633 # so rmtree works.
634 try:
635 platform_utils.rmtree(os.path.join(path, '.git'))
636 except OSError as e:
637 print('Failed to remove %s (%s)' % (os.path.join(path, '.git'), str(e)), file=sys.stderr)
638 print('error: Failed to delete obsolete path %s' % path, file=sys.stderr)
639 print(' remove manually, then run sync again', file=sys.stderr)
640 return 1
641
642 # Delete everything under the worktree, except for directories that contain
643 # another git project
644 dirs_to_remove = []
645 failed = False
646 for root, dirs, files in platform_utils.walk(path):
647 for f in files:
648 try:
649 platform_utils.remove(os.path.join(root, f))
650 except OSError as e:
651 print('Failed to remove %s (%s)' % (os.path.join(root, f), str(e)), file=sys.stderr)
652 failed = True
653 dirs[:] = [d for d in dirs
654 if not os.path.lexists(os.path.join(root, d, '.git'))]
655 dirs_to_remove += [os.path.join(root, d) for d in dirs
656 if os.path.join(root, d) not in dirs_to_remove]
657 for d in reversed(dirs_to_remove):
658 if platform_utils.islink(d):
659 try:
660 platform_utils.remove(d)
661 except OSError as e:
662 print('Failed to remove %s (%s)' % (os.path.join(root, d), str(e)), file=sys.stderr)
663 failed = True
664 elif len(platform_utils.listdir(d)) == 0:
665 try:
666 platform_utils.rmdir(d)
667 except OSError as e:
668 print('Failed to remove %s (%s)' % (os.path.join(root, d), str(e)), file=sys.stderr)
669 failed = True
670 continue
671 if failed:
672 print('error: Failed to delete obsolete path %s' % path, file=sys.stderr)
673 print(' remove manually, then run sync again', file=sys.stderr)
674 return 1
675
676 # Try deleting parent dirs if they are empty
677 project_dir = path
678 while project_dir != self.manifest.topdir:
679 if len(platform_utils.listdir(project_dir)) == 0:
680 platform_utils.rmdir(project_dir)
681 else:
682 break
683 project_dir = os.path.dirname(project_dir)
684
685 return 0
686
687 def UpdateProjectList(self, opt): 686 def UpdateProjectList(self, opt):
688 new_project_paths = [] 687 new_project_paths = []
689 for project in self.GetProjects(None, missing_ok=True): 688 for project in self.GetProjects(None, missing_ok=True):
690 if project.relpath: 689 if project.relpath:
691 new_project_paths.append(project.relpath) 690 new_project_paths.append(project.relpath)
692 file_name = 'project.list' 691 file_name = 'project.list'
693 file_path = os.path.join(self.manifest.repodir, file_name) 692 file_path = os.path.join(self.repodir, file_name)
694 old_project_paths = [] 693 old_project_paths = []
695 694
696 if os.path.exists(file_path): 695 if os.path.exists(file_path):
@@ -705,28 +704,20 @@ later is required to fix a server side protocol bug.
705 gitdir = os.path.join(self.manifest.topdir, path, '.git') 704 gitdir = os.path.join(self.manifest.topdir, path, '.git')
706 if os.path.exists(gitdir): 705 if os.path.exists(gitdir):
707 project = Project( 706 project = Project(
708 manifest = self.manifest, 707 manifest=self.manifest,
709 name = path, 708 name=path,
710 remote = RemoteSpec('origin'), 709 remote=RemoteSpec('origin'),
711 gitdir = gitdir, 710 gitdir=gitdir,
712 objdir = gitdir, 711 objdir=gitdir,
713 worktree = os.path.join(self.manifest.topdir, path), 712 use_git_worktrees=os.path.isfile(gitdir),
714 relpath = path, 713 worktree=os.path.join(self.manifest.topdir, path),
715 revisionExpr = 'HEAD', 714 relpath=path,
716 revisionId = None, 715 revisionExpr='HEAD',
717 groups = None) 716 revisionId=None,
718 717 groups=None)
719 if project.IsDirty() and opt.force_remove_dirty: 718 if not project.DeleteWorktree(
720 print('WARNING: Removing dirty project "%s": uncommitted changes ' 719 quiet=opt.quiet,
721 'erased' % project.relpath, file=sys.stderr) 720 force=opt.force_remove_dirty):
722 self._DeleteProject(project.worktree)
723 elif project.IsDirty():
724 print('error: Cannot remove project "%s": uncommitted changes '
725 'are present' % project.relpath, file=sys.stderr)
726 print(' commit changes, then run sync again',
727 file=sys.stderr)
728 return 1
729 elif self._DeleteProject(project.worktree):
730 return 1 721 return 1
731 722
732 new_project_paths.sort() 723 new_project_paths.sort()
@@ -735,6 +726,56 @@ later is required to fix a server side protocol bug.
735 fd.write('\n') 726 fd.write('\n')
736 return 0 727 return 0
737 728
729 def UpdateCopyLinkfileList(self):
730 """Save all dests of copyfile and linkfile, and update them if needed.
731
732 Returns:
733 Whether update was successful.
734 """
735 new_paths = {}
736 new_linkfile_paths = []
737 new_copyfile_paths = []
738 for project in self.GetProjects(None, missing_ok=True):
739 new_linkfile_paths.extend(x.dest for x in project.linkfiles)
740 new_copyfile_paths.extend(x.dest for x in project.copyfiles)
741
742 new_paths = {
743 'linkfile': new_linkfile_paths,
744 'copyfile': new_copyfile_paths,
745 }
746
747 copylinkfile_name = 'copy-link-files.json'
748 copylinkfile_path = os.path.join(self.manifest.repodir, copylinkfile_name)
749 old_copylinkfile_paths = {}
750
751 if os.path.exists(copylinkfile_path):
752 with open(copylinkfile_path, 'rb') as fp:
753 try:
754 old_copylinkfile_paths = json.load(fp)
755 except:
756 print('error: %s is not a json formatted file.' %
757 copylinkfile_path, file=sys.stderr)
758 platform_utils.remove(copylinkfile_path)
759 return False
760
761 need_remove_files = []
762 need_remove_files.extend(
763 set(old_copylinkfile_paths.get('linkfile', [])) -
764 set(new_linkfile_paths))
765 need_remove_files.extend(
766 set(old_copylinkfile_paths.get('copyfile', [])) -
767 set(new_copyfile_paths))
768
769 for need_remove_file in need_remove_files:
770 # Try to remove the updated copyfile or linkfile.
771 # So, if the file is not exist, nothing need to do.
772 platform_utils.remove(need_remove_file, missing_ok=True)
773
774 # Create copy-link-files.json, save dest path of "copyfile" and "linkfile".
775 with open(copylinkfile_path, 'w', encoding='utf-8') as fp:
776 json.dump(new_paths, fp)
777 return True
778
738 def _SmartSyncSetup(self, opt, smart_sync_manifest_path): 779 def _SmartSyncSetup(self, opt, smart_sync_manifest_path):
739 if not self.manifest.manifest_server: 780 if not self.manifest.manifest_server:
740 print('error: cannot smart sync: no manifest server defined in ' 781 print('error: cannot smart sync: no manifest server defined in '
@@ -745,7 +786,7 @@ later is required to fix a server side protocol bug.
745 if not opt.quiet: 786 if not opt.quiet:
746 print('Using manifest server %s' % manifest_server) 787 print('Using manifest server %s' % manifest_server)
747 788
748 if not '@' in manifest_server: 789 if '@' not in manifest_server:
749 username = None 790 username = None
750 password = None 791 password = None
751 if opt.manifest_server_username and opt.manifest_server_password: 792 if opt.manifest_server_username and opt.manifest_server_password:
@@ -782,19 +823,15 @@ later is required to fix a server side protocol bug.
782 try: 823 try:
783 server = xmlrpc.client.Server(manifest_server, transport=transport) 824 server = xmlrpc.client.Server(manifest_server, transport=transport)
784 if opt.smart_sync: 825 if opt.smart_sync:
785 p = self.manifest.manifestProject 826 branch = self._GetBranch()
786 b = p.GetBranch(p.CurrentBranch) 827
787 branch = b.merge 828 if 'SYNC_TARGET' in os.environ:
788 if branch.startswith(R_HEADS): 829 target = os.environ['SYNC_TARGET']
789 branch = branch[len(R_HEADS):]
790
791 env = os.environ.copy()
792 if 'SYNC_TARGET' in env:
793 target = env['SYNC_TARGET']
794 [success, manifest_str] = server.GetApprovedManifest(branch, target) 830 [success, manifest_str] = server.GetApprovedManifest(branch, target)
795 elif 'TARGET_PRODUCT' in env and 'TARGET_BUILD_VARIANT' in env: 831 elif ('TARGET_PRODUCT' in os.environ and
796 target = '%s-%s' % (env['TARGET_PRODUCT'], 832 'TARGET_BUILD_VARIANT' in os.environ):
797 env['TARGET_BUILD_VARIANT']) 833 target = '%s-%s' % (os.environ['TARGET_PRODUCT'],
834 os.environ['TARGET_BUILD_VARIANT'])
798 [success, manifest_str] = server.GetApprovedManifest(branch, target) 835 [success, manifest_str] = server.GetApprovedManifest(branch, target)
799 else: 836 else:
800 [success, manifest_str] = server.GetApprovedManifest(branch) 837 [success, manifest_str] = server.GetApprovedManifest(branch)
@@ -833,12 +870,15 @@ later is required to fix a server side protocol bug.
833 """Fetch & update the local manifest project.""" 870 """Fetch & update the local manifest project."""
834 if not opt.local_only: 871 if not opt.local_only:
835 start = time.time() 872 start = time.time()
836 success = mp.Sync_NetworkHalf(quiet=opt.quiet, 873 success = mp.Sync_NetworkHalf(quiet=opt.quiet, verbose=opt.verbose,
837 current_branch_only=opt.current_branch_only, 874 current_branch_only=self._GetCurrentBranchOnly(opt),
838 no_tags=opt.no_tags, 875 force_sync=opt.force_sync,
876 tags=opt.tags,
839 optimized_fetch=opt.optimized_fetch, 877 optimized_fetch=opt.optimized_fetch,
878 retry_fetches=opt.retry_fetches,
840 submodules=self.manifest.HasSubmodules, 879 submodules=self.manifest.HasSubmodules,
841 clone_filter=self.manifest.CloneFilter) 880 clone_filter=self.manifest.CloneFilter,
881 partial_clone_exclude=self.manifest.PartialCloneExclude)
842 finish = time.time() 882 finish = time.time()
843 self.event_log.AddSync(mp, event_log.TASK_SYNC_NETWORK, 883 self.event_log.AddSync(mp, event_log.TASK_SYNC_NETWORK,
844 start, finish, success) 884 start, finish, success)
@@ -852,7 +892,7 @@ later is required to fix a server side protocol bug.
852 start, time.time(), clean) 892 start, time.time(), clean)
853 if not clean: 893 if not clean:
854 sys.exit(1) 894 sys.exit(1)
855 self._ReloadManifest(opt.manifest_name) 895 self._ReloadManifest(manifest_name)
856 if opt.jobs is None: 896 if opt.jobs is None:
857 self.jobs = self.manifest.default.sync_j 897 self.jobs = self.manifest.default.sync_j
858 898
@@ -886,7 +926,10 @@ later is required to fix a server side protocol bug.
886 926
887 manifest_name = opt.manifest_name 927 manifest_name = opt.manifest_name
888 smart_sync_manifest_path = os.path.join( 928 smart_sync_manifest_path = os.path.join(
889 self.manifest.manifestProject.worktree, 'smart_sync_override.xml') 929 self.manifest.manifestProject.worktree, 'smart_sync_override.xml')
930
931 if opt.clone_bundle is None:
932 opt.clone_bundle = self.manifest.CloneBundle
890 933
891 if opt.smart_sync or opt.smart_tag: 934 if opt.smart_sync or opt.smart_tag:
892 manifest_name = self._SmartSyncSetup(opt, smart_sync_manifest_path) 935 manifest_name = self._SmartSyncSetup(opt, smart_sync_manifest_path)
@@ -898,8 +941,17 @@ later is required to fix a server side protocol bug.
898 print('error: failed to remove existing smart sync override manifest: %s' % 941 print('error: failed to remove existing smart sync override manifest: %s' %
899 e, file=sys.stderr) 942 e, file=sys.stderr)
900 943
944 err_event = multiprocessing.Event()
945
901 rp = self.manifest.repoProject 946 rp = self.manifest.repoProject
902 rp.PreSync() 947 rp.PreSync()
948 cb = rp.CurrentBranch
949 if cb:
950 base = rp.GetBranch(cb).merge
951 if not base or not base.startswith('refs/heads/'):
952 print('warning: repo is not tracking a remote branch, so it will not '
953 'receive updates; run `repo init --repo-rev=stable` to fix.',
954 file=sys.stderr)
903 955
904 mp = self.manifest.manifestProject 956 mp = self.manifest.manifestProject
905 mp.PreSync() 957 mp.PreSync()
@@ -907,7 +959,21 @@ later is required to fix a server side protocol bug.
907 if opt.repo_upgraded: 959 if opt.repo_upgraded:
908 _PostRepoUpgrade(self.manifest, quiet=opt.quiet) 960 _PostRepoUpgrade(self.manifest, quiet=opt.quiet)
909 961
910 self._UpdateManifestProject(opt, mp, manifest_name) 962 if not opt.mp_update:
963 print('Skipping update of local manifest project.')
964 else:
965 self._UpdateManifestProject(opt, mp, manifest_name)
966
967 load_local_manifests = not self.manifest.HasLocalManifests
968 use_superproject = git_superproject.UseSuperproject(opt, self.manifest)
969 superproject_logging_data = {
970 'superproject': use_superproject,
971 'haslocalmanifests': bool(self.manifest.HasLocalManifests),
972 'hassuperprojecttag': bool(self.manifest.superproject),
973 }
974 if use_superproject:
975 manifest_name = self._UpdateProjectsRevisionId(
976 opt, args, load_local_manifests, superproject_logging_data) or opt.manifest_name
911 977
912 if self.gitc_manifest: 978 if self.gitc_manifest:
913 gitc_manifest_projects = self.GetProjects(args, 979 gitc_manifest_projects = self.GetProjects(args,
@@ -948,56 +1014,92 @@ later is required to fix a server side protocol bug.
948 missing_ok=True, 1014 missing_ok=True,
949 submodules_ok=opt.fetch_submodules) 1015 submodules_ok=opt.fetch_submodules)
950 1016
1017 err_network_sync = False
1018 err_update_projects = False
1019
951 self._fetch_times = _FetchTimes(self.manifest) 1020 self._fetch_times = _FetchTimes(self.manifest)
952 if not opt.local_only: 1021 if not opt.local_only:
953 to_fetch = [] 1022 with multiprocessing.Manager() as manager:
954 now = time.time() 1023 with ssh.ProxyManager(manager) as ssh_proxy:
955 if _ONE_DAY_S <= (now - rp.LastFetch): 1024 # Initialize the socket dir once in the parent.
956 to_fetch.append(rp) 1025 ssh_proxy.sock()
957 to_fetch.extend(all_projects) 1026 all_projects = self._FetchMain(opt, args, all_projects, err_event,
958 to_fetch.sort(key=self._fetch_times.Get, reverse=True) 1027 manifest_name, load_local_manifests,
959 1028 ssh_proxy)
960 fetched = self._Fetch(to_fetch, opt) 1029
961 _PostRepoFetch(rp, opt.no_repo_verify)
962 if opt.network_only: 1030 if opt.network_only:
963 # bail out now; the rest touches the working tree
964 return 1031 return
965 1032
966 # Iteratively fetch missing and/or nested unregistered submodules 1033 # If we saw an error, exit with code 1 so that other scripts can check.
967 previously_missing_set = set() 1034 if err_event.is_set():
968 while True: 1035 err_network_sync = True
969 self._ReloadManifest(manifest_name) 1036 if opt.fail_fast:
970 all_projects = self.GetProjects(args, 1037 print('\nerror: Exited sync due to fetch errors.\n'
971 missing_ok=True, 1038 'Local checkouts *not* updated. Resolve network issues & '
972 submodules_ok=opt.fetch_submodules) 1039 'retry.\n'
973 missing = [] 1040 '`repo sync -l` will update some local checkouts.',
974 for project in all_projects: 1041 file=sys.stderr)
975 if project.gitdir not in fetched: 1042 sys.exit(1)
976 missing.append(project)
977 if not missing:
978 break
979 # Stop us from non-stopped fetching actually-missing repos: If set of
980 # missing repos has not been changed from last fetch, we break.
981 missing_set = set(p.name for p in missing)
982 if previously_missing_set == missing_set:
983 break
984 previously_missing_set = missing_set
985 fetched.update(self._Fetch(missing, opt))
986 1043
987 if self.manifest.IsMirror or self.manifest.IsArchive: 1044 if self.manifest.IsMirror or self.manifest.IsArchive:
988 # bail out now, we have no working tree 1045 # bail out now, we have no working tree
989 return 1046 return
990 1047
991 if self.UpdateProjectList(opt): 1048 if self.UpdateProjectList(opt):
992 sys.exit(1) 1049 err_event.set()
1050 err_update_projects = True
1051 if opt.fail_fast:
1052 print('\nerror: Local checkouts *not* updated.', file=sys.stderr)
1053 sys.exit(1)
993 1054
994 self._Checkout(all_projects, opt) 1055 err_update_linkfiles = not self.UpdateCopyLinkfileList()
1056 if err_update_linkfiles:
1057 err_event.set()
1058 if opt.fail_fast:
1059 print('\nerror: Local update copyfile or linkfile failed.', file=sys.stderr)
1060 sys.exit(1)
1061
1062 err_results = []
1063 # NB: We don't exit here because this is the last step.
1064 err_checkout = not self._Checkout(all_projects, opt, err_results)
1065 if err_checkout:
1066 err_event.set()
995 1067
996 # If there's a notice that's supposed to print at the end of the sync, print 1068 # If there's a notice that's supposed to print at the end of the sync, print
997 # it now... 1069 # it now...
998 if self.manifest.notice: 1070 if self.manifest.notice:
999 print(self.manifest.notice) 1071 print(self.manifest.notice)
1000 1072
1073 # If we saw an error, exit with code 1 so that other scripts can check.
1074 if err_event.is_set():
1075 print('\nerror: Unable to fully sync the tree.', file=sys.stderr)
1076 if err_network_sync:
1077 print('error: Downloading network changes failed.', file=sys.stderr)
1078 if err_update_projects:
1079 print('error: Updating local project lists failed.', file=sys.stderr)
1080 if err_update_linkfiles:
1081 print('error: Updating copyfiles or linkfiles failed.', file=sys.stderr)
1082 if err_checkout:
1083 print('error: Checking out local projects failed.', file=sys.stderr)
1084 if err_results:
1085 print('Failing repos:\n%s' % '\n'.join(err_results), file=sys.stderr)
1086 print('Try re-running with "-j1 --fail-fast" to exit at the first error.',
1087 file=sys.stderr)
1088 sys.exit(1)
1089
1090 # Log the previous sync analysis state from the config.
1091 self.git_event_log.LogDataConfigEvents(mp.config.GetSyncAnalysisStateData(),
1092 'previous_sync_state')
1093
1094 # Update and log with the new sync analysis state.
1095 mp.config.UpdateSyncAnalysisState(opt, superproject_logging_data)
1096 self.git_event_log.LogDataConfigEvents(mp.config.GetSyncAnalysisStateData(),
1097 'current_sync_state')
1098
1099 if not opt.quiet:
1100 print('repo sync has finished successfully.')
1101
1102
1001def _PostRepoUpgrade(manifest, quiet=False): 1103def _PostRepoUpgrade(manifest, quiet=False):
1002 wrapper = Wrapper() 1104 wrapper = Wrapper()
1003 if wrapper.NeedSetupGnuPG(): 1105 if wrapper.NeedSetupGnuPG():
@@ -1006,15 +1108,29 @@ def _PostRepoUpgrade(manifest, quiet=False):
1006 if project.Exists: 1108 if project.Exists:
1007 project.PostRepoUpgrade() 1109 project.PostRepoUpgrade()
1008 1110
1009def _PostRepoFetch(rp, no_repo_verify=False, verbose=False): 1111
1112def _PostRepoFetch(rp, repo_verify=True, verbose=False):
1010 if rp.HasChanges: 1113 if rp.HasChanges:
1011 print('info: A new version of repo is available', file=sys.stderr) 1114 print('info: A new version of repo is available', file=sys.stderr)
1012 print(file=sys.stderr) 1115 wrapper = Wrapper()
1013 if no_repo_verify or _VerifyTag(rp): 1116 try:
1014 syncbuf = SyncBuffer(rp.config) 1117 rev = rp.bare_git.describe(rp.GetRevisionId())
1015 rp.Sync_LocalHalf(syncbuf) 1118 except GitError:
1016 if not syncbuf.Finish(): 1119 rev = None
1017 sys.exit(1) 1120 _, new_rev = wrapper.check_repo_rev(rp.gitdir, rev, repo_verify=repo_verify)
1121 # See if we're held back due to missing signed tag.
1122 current_revid = rp.bare_git.rev_parse('HEAD')
1123 new_revid = rp.bare_git.rev_parse('--verify', new_rev)
1124 if current_revid != new_revid:
1125 # We want to switch to the new rev, but also not trash any uncommitted
1126 # changes. This helps with local testing/hacking.
1127 # If a local change has been made, we will throw that away.
1128 # We also have to make sure this will switch to an older commit if that's
1129 # the latest tag in order to support release rollback.
1130 try:
1131 rp.work_git.reset('--keep', new_rev)
1132 except GitError as e:
1133 sys.exit(str(e))
1018 print('info: Restarting repo with latest version', file=sys.stderr) 1134 print('info: Restarting repo with latest version', file=sys.stderr)
1019 raise RepoChangedException(['--repo-upgraded']) 1135 raise RepoChangedException(['--repo-upgraded'])
1020 else: 1136 else:
@@ -1024,53 +1140,6 @@ def _PostRepoFetch(rp, no_repo_verify=False, verbose=False):
1024 print('repo version %s is current' % rp.work_git.describe(HEAD), 1140 print('repo version %s is current' % rp.work_git.describe(HEAD),
1025 file=sys.stderr) 1141 file=sys.stderr)
1026 1142
1027def _VerifyTag(project):
1028 gpg_dir = os.path.expanduser('~/.repoconfig/gnupg')
1029 if not os.path.exists(gpg_dir):
1030 print('warning: GnuPG was not available during last "repo init"\n'
1031 'warning: Cannot automatically authenticate repo."""',
1032 file=sys.stderr)
1033 return True
1034
1035 try:
1036 cur = project.bare_git.describe(project.GetRevisionId())
1037 except GitError:
1038 cur = None
1039
1040 if not cur \
1041 or re.compile(r'^.*-[0-9]{1,}-g[0-9a-f]{1,}$').match(cur):
1042 rev = project.revisionExpr
1043 if rev.startswith(R_HEADS):
1044 rev = rev[len(R_HEADS):]
1045
1046 print(file=sys.stderr)
1047 print("warning: project '%s' branch '%s' is not signed"
1048 % (project.name, rev), file=sys.stderr)
1049 return False
1050
1051 env = os.environ.copy()
1052 env['GIT_DIR'] = project.gitdir.encode()
1053 env['GNUPGHOME'] = gpg_dir.encode()
1054
1055 cmd = [GIT, 'tag', '-v', cur]
1056 proc = subprocess.Popen(cmd,
1057 stdout = subprocess.PIPE,
1058 stderr = subprocess.PIPE,
1059 env = env)
1060 out = proc.stdout.read()
1061 proc.stdout.close()
1062
1063 err = proc.stderr.read()
1064 proc.stderr.close()
1065
1066 if proc.wait() != 0:
1067 print(file=sys.stderr)
1068 print(out, file=sys.stderr)
1069 print(err, file=sys.stderr)
1070 print(file=sys.stderr)
1071 return False
1072 return True
1073
1074 1143
1075class _FetchTimes(object): 1144class _FetchTimes(object):
1076 _ALPHA = 0.5 1145 _ALPHA = 0.5
@@ -1090,7 +1159,7 @@ class _FetchTimes(object):
1090 old = self._times.get(name, t) 1159 old = self._times.get(name, t)
1091 self._seen.add(name) 1160 self._seen.add(name)
1092 a = self._ALPHA 1161 a = self._ALPHA
1093 self._times[name] = (a*t) + ((1-a) * old) 1162 self._times[name] = (a * t) + ((1 - a) * old)
1094 1163
1095 def _Load(self): 1164 def _Load(self):
1096 if self._times is None: 1165 if self._times is None:
@@ -1098,10 +1167,7 @@ class _FetchTimes(object):
1098 with open(self._path) as f: 1167 with open(self._path) as f:
1099 self._times = json.load(f) 1168 self._times = json.load(f)
1100 except (IOError, ValueError): 1169 except (IOError, ValueError):
1101 try: 1170 platform_utils.remove(self._path, missing_ok=True)
1102 platform_utils.remove(self._path)
1103 except OSError:
1104 pass
1105 self._times = {} 1171 self._times = {}
1106 1172
1107 def Save(self): 1173 def Save(self):
@@ -1119,15 +1185,14 @@ class _FetchTimes(object):
1119 with open(self._path, 'w') as f: 1185 with open(self._path, 'w') as f:
1120 json.dump(self._times, f, indent=2) 1186 json.dump(self._times, f, indent=2)
1121 except (IOError, TypeError): 1187 except (IOError, TypeError):
1122 try: 1188 platform_utils.remove(self._path, missing_ok=True)
1123 platform_utils.remove(self._path)
1124 except OSError:
1125 pass
1126 1189
1127# This is a replacement for xmlrpc.client.Transport using urllib2 1190# This is a replacement for xmlrpc.client.Transport using urllib2
1128# and supporting persistent-http[s]. It cannot change hosts from 1191# and supporting persistent-http[s]. It cannot change hosts from
1129# request to request like the normal transport, the real url 1192# request to request like the normal transport, the real url
1130# is passed during initialization. 1193# is passed during initialization.
1194
1195
1131class PersistentTransport(xmlrpc.client.Transport): 1196class PersistentTransport(xmlrpc.client.Transport):
1132 def __init__(self, orig_host): 1197 def __init__(self, orig_host):
1133 self.orig_host = orig_host 1198 self.orig_host = orig_host
@@ -1138,7 +1203,7 @@ class PersistentTransport(xmlrpc.client.Transport):
1138 # Since we're only using them for HTTP, copy the file temporarily, 1203 # Since we're only using them for HTTP, copy the file temporarily,
1139 # stripping those prefixes away. 1204 # stripping those prefixes away.
1140 if cookiefile: 1205 if cookiefile:
1141 tmpcookiefile = tempfile.NamedTemporaryFile() 1206 tmpcookiefile = tempfile.NamedTemporaryFile(mode='w')
1142 tmpcookiefile.write("# HTTP Cookie File") 1207 tmpcookiefile.write("# HTTP Cookie File")
1143 try: 1208 try:
1144 with open(cookiefile) as f: 1209 with open(cookiefile) as f:
@@ -1162,7 +1227,7 @@ class PersistentTransport(xmlrpc.client.Transport):
1162 if proxy: 1227 if proxy:
1163 proxyhandler = urllib.request.ProxyHandler({ 1228 proxyhandler = urllib.request.ProxyHandler({
1164 "http": proxy, 1229 "http": proxy,
1165 "https": proxy }) 1230 "https": proxy})
1166 1231
1167 opener = urllib.request.build_opener( 1232 opener = urllib.request.build_opener(
1168 urllib.request.HTTPCookieProcessor(cookiejar), 1233 urllib.request.HTTPCookieProcessor(cookiejar),
@@ -1219,4 +1284,3 @@ class PersistentTransport(xmlrpc.client.Transport):
1219 1284
1220 def close(self): 1285 def close(self):
1221 pass 1286 pass
1222
diff --git a/subcmds/upload.py b/subcmds/upload.py
index 5c12aaee..c48deab6 100644
--- a/subcmds/upload.py
+++ b/subcmds/upload.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,25 +12,23 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function
18import copy 15import copy
16import functools
17import optparse
19import re 18import re
20import sys 19import sys
21 20
22from command import InteractiveCommand 21from command import DEFAULT_LOCAL_JOBS, InteractiveCommand
23from editor import Editor 22from editor import Editor
24from error import HookError, UploadError 23from error import UploadError
25from git_command import GitCommand 24from git_command import GitCommand
26from project import RepoHook 25from git_refs import R_HEADS
26from hooks import RepoHook
27 27
28from pyversion import is_python3
29if not is_python3():
30 input = raw_input
31else:
32 unicode = str
33 28
34UNUSUAL_COMMIT_THRESHOLD = 5 29UNUSUAL_COMMIT_THRESHOLD = 5
35 30
31
36def _ConfirmManyUploads(multiple_branches=False): 32def _ConfirmManyUploads(multiple_branches=False):
37 if multiple_branches: 33 if multiple_branches:
38 print('ATTENTION: One or more branches has an unusually high number ' 34 print('ATTENTION: One or more branches has an unusually high number '
@@ -44,19 +40,22 @@ def _ConfirmManyUploads(multiple_branches=False):
44 answer = input("If you are sure you intend to do this, type 'yes': ").strip() 40 answer = input("If you are sure you intend to do this, type 'yes': ").strip()
45 return answer == "yes" 41 return answer == "yes"
46 42
43
47def _die(fmt, *args): 44def _die(fmt, *args):
48 msg = fmt % args 45 msg = fmt % args
49 print('error: %s' % msg, file=sys.stderr) 46 print('error: %s' % msg, file=sys.stderr)
50 sys.exit(1) 47 sys.exit(1)
51 48
49
52def _SplitEmails(values): 50def _SplitEmails(values):
53 result = [] 51 result = []
54 for value in values: 52 for value in values:
55 result.extend([s.strip() for s in value.split(',')]) 53 result.extend([s.strip() for s in value.split(',')])
56 return result 54 return result
57 55
56
58class Upload(InteractiveCommand): 57class Upload(InteractiveCommand):
59 common = True 58 COMMON = True
60 helpSummary = "Upload changes for code review" 59 helpSummary = "Upload changes for code review"
61 helpUsage = """ 60 helpUsage = """
62%prog [--re --cc] [<project>]... 61%prog [--re --cc] [<project>]...
@@ -126,74 +125,89 @@ is set to "true" then repo will assume you always want the equivalent
126of the -t option to the repo command. If unset or set to "false" then 125of the -t option to the repo command. If unset or set to "false" then
127repo will make use of only the command line option. 126repo will make use of only the command line option.
128 127
128review.URL.uploadhashtags:
129
130To add hashtags whenever uploading a commit, you can set a per-project
131or global Git option to do so. The value of review.URL.uploadhashtags
132will be used as comma delimited hashtags like the --hashtag option.
133
134review.URL.uploadlabels:
135
136To add labels whenever uploading a commit, you can set a per-project
137or global Git option to do so. The value of review.URL.uploadlabels
138will be used as comma delimited labels like the --label option.
139
140review.URL.uploadnotify:
141
142Control e-mail notifications when uploading.
143https://gerrit-review.googlesource.com/Documentation/user-upload.html#notify
144
129# References 145# References
130 146
131Gerrit Code Review: https://www.gerritcodereview.com/ 147Gerrit Code Review: https://www.gerritcodereview.com/
132 148
133""" 149"""
150 PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
134 151
135 def _Options(self, p): 152 def _Options(self, p):
136 p.add_option('-t', 153 p.add_option('-t',
137 dest='auto_topic', action='store_true', 154 dest='auto_topic', action='store_true',
138 help='Send local branch name to Gerrit Code Review') 155 help='send local branch name to Gerrit Code Review')
156 p.add_option('--hashtag', '--ht',
157 dest='hashtags', action='append', default=[],
158 help='add hashtags (comma delimited) to the review')
159 p.add_option('--hashtag-branch', '--htb',
160 action='store_true',
161 help='add local branch name as a hashtag')
162 p.add_option('-l', '--label',
163 dest='labels', action='append', default=[],
164 help='add a label when uploading')
139 p.add_option('--re', '--reviewers', 165 p.add_option('--re', '--reviewers',
140 type='string', action='append', dest='reviewers', 166 type='string', action='append', dest='reviewers',
141 help='Request reviews from these people.') 167 help='request reviews from these people')
142 p.add_option('--cc', 168 p.add_option('--cc',
143 type='string', action='append', dest='cc', 169 type='string', action='append', dest='cc',
144 help='Also send email to these email addresses.') 170 help='also send email to these email addresses')
145 p.add_option('--br', 171 p.add_option('--br', '--branch',
146 type='string', action='store', dest='branch', 172 type='string', action='store', dest='branch',
147 help='Branch to upload.') 173 help='(local) branch to upload')
148 p.add_option('--cbr', '--current-branch', 174 p.add_option('-c', '--current-branch',
175 dest='current_branch', action='store_true',
176 help='upload current git branch')
177 p.add_option('--no-current-branch',
178 dest='current_branch', action='store_false',
179 help='upload all git branches')
180 # Turn this into a warning & remove this someday.
181 p.add_option('--cbr',
149 dest='current_branch', action='store_true', 182 dest='current_branch', action='store_true',
150 help='Upload current git branch.') 183 help=optparse.SUPPRESS_HELP)
151 p.add_option('-d', '--draft',
152 action='store_true', dest='draft', default=False,
153 help='If specified, upload as a draft.')
154 p.add_option('--ne', '--no-emails', 184 p.add_option('--ne', '--no-emails',
155 action='store_false', dest='notify', default=True, 185 action='store_false', dest='notify', default=True,
156 help='If specified, do not send emails on upload.') 186 help='do not send e-mails on upload')
157 p.add_option('-p', '--private', 187 p.add_option('-p', '--private',
158 action='store_true', dest='private', default=False, 188 action='store_true', dest='private', default=False,
159 help='If specified, upload as a private change.') 189 help='upload as a private change (deprecated; use --wip)')
160 p.add_option('-w', '--wip', 190 p.add_option('-w', '--wip',
161 action='store_true', dest='wip', default=False, 191 action='store_true', dest='wip', default=False,
162 help='If specified, upload as a work-in-progress change.') 192 help='upload as a work-in-progress change')
163 p.add_option('-o', '--push-option', 193 p.add_option('-o', '--push-option',
164 type='string', action='append', dest='push_options', 194 type='string', action='append', dest='push_options',
165 default=[], 195 default=[],
166 help='Additional push options to transmit') 196 help='additional push options to transmit')
167 p.add_option('-D', '--destination', '--dest', 197 p.add_option('-D', '--destination', '--dest',
168 type='string', action='store', dest='dest_branch', 198 type='string', action='store', dest='dest_branch',
169 metavar='BRANCH', 199 metavar='BRANCH',
170 help='Submit for review on this target branch.') 200 help='submit for review on this target branch')
171 201 p.add_option('-n', '--dry-run',
172 # Options relating to upload hook. Note that verify and no-verify are NOT 202 dest='dryrun', default=False, action='store_true',
173 # opposites of each other, which is why they store to different locations. 203 help='do everything except actually upload the CL')
174 # We are using them to match 'git commit' syntax. 204 p.add_option('-y', '--yes',
175 # 205 default=False, action='store_true',
176 # Combinations: 206 help='answer yes to all safe prompts')
177 # - no-verify=False, verify=False (DEFAULT):
178 # If stdout is a tty, can prompt about running upload hooks if needed.
179 # If user denies running hooks, the upload is cancelled. If stdout is
180 # not a tty and we would need to prompt about upload hooks, upload is
181 # cancelled.
182 # - no-verify=False, verify=True:
183 # Always run upload hooks with no prompt.
184 # - no-verify=True, verify=False:
185 # Never run upload hooks, but upload anyway (AKA bypass hooks).
186 # - no-verify=True, verify=True:
187 # Invalid
188 p.add_option('--no-cert-checks', 207 p.add_option('--no-cert-checks',
189 dest='validate_certs', action='store_false', default=True, 208 dest='validate_certs', action='store_false', default=True,
190 help='Disable verifying ssl certs (unsafe).') 209 help='disable verifying ssl certs (unsafe)')
191 p.add_option('--no-verify', 210 RepoHook.AddOptionGroup(p, 'pre-upload')
192 dest='bypass_hooks', action='store_true',
193 help='Do not run the upload hook.')
194 p.add_option('--verify',
195 dest='allow_all_hooks', action='store_true',
196 help='Run the upload hook without prompting.')
197 211
198 def _SingleBranch(self, opt, branch, people): 212 def _SingleBranch(self, opt, branch, people):
199 project = branch.project 213 project = branch.project
@@ -212,20 +226,24 @@ Gerrit Code Review: https://www.gerritcodereview.com/
212 226
213 destination = opt.dest_branch or project.dest_branch or project.revisionExpr 227 destination = opt.dest_branch or project.dest_branch or project.revisionExpr
214 print('Upload project %s/ to remote branch %s%s:' % 228 print('Upload project %s/ to remote branch %s%s:' %
215 (project.relpath, destination, ' (draft)' if opt.draft else '')) 229 (project.relpath, destination, ' (private)' if opt.private else ''))
216 print(' branch %s (%2d commit%s, %s):' % ( 230 print(' branch %s (%2d commit%s, %s):' % (
217 name, 231 name,
218 len(commit_list), 232 len(commit_list),
219 len(commit_list) != 1 and 's' or '', 233 len(commit_list) != 1 and 's' or '',
220 date)) 234 date))
221 for commit in commit_list: 235 for commit in commit_list:
222 print(' %s' % commit) 236 print(' %s' % commit)
223 237
224 print('to %s (y/N)? ' % remote.review, end='') 238 print('to %s (y/N)? ' % remote.review, end='')
225 # TODO: When we require Python 3, use flush=True w/print above. 239 # TODO: When we require Python 3, use flush=True w/print above.
226 sys.stdout.flush() 240 sys.stdout.flush()
227 answer = sys.stdin.readline().strip().lower() 241 if opt.yes:
228 answer = answer in ('y', 'yes', '1', 'true', 't') 242 print('<--yes>')
243 answer = True
244 else:
245 answer = sys.stdin.readline().strip().lower()
246 answer = answer in ('y', 'yes', '1', 'true', 't')
229 247
230 if answer: 248 if answer:
231 if len(branch.commits) > UNUSUAL_COMMIT_THRESHOLD: 249 if len(branch.commits) > UNUSUAL_COMMIT_THRESHOLD:
@@ -322,12 +340,12 @@ Gerrit Code Review: https://www.gerritcodereview.com/
322 340
323 key = 'review.%s.autoreviewer' % project.GetBranch(name).remote.review 341 key = 'review.%s.autoreviewer' % project.GetBranch(name).remote.review
324 raw_list = project.config.GetString(key) 342 raw_list = project.config.GetString(key)
325 if not raw_list is None: 343 if raw_list is not None:
326 people[0].extend([entry.strip() for entry in raw_list.split(',')]) 344 people[0].extend([entry.strip() for entry in raw_list.split(',')])
327 345
328 key = 'review.%s.autocopy' % project.GetBranch(name).remote.review 346 key = 'review.%s.autocopy' % project.GetBranch(name).remote.review
329 raw_list = project.config.GetString(key) 347 raw_list = project.config.GetString(key)
330 if not raw_list is None and len(people[0]) > 0: 348 if raw_list is not None and len(people[0]) > 0:
331 people[1].extend([entry.strip() for entry in raw_list.split(',')]) 349 people[1].extend([entry.strip() for entry in raw_list.split(',')])
332 350
333 def _FindGerritChange(self, branch): 351 def _FindGerritChange(self, branch):
@@ -364,7 +382,11 @@ Gerrit Code Review: https://www.gerritcodereview.com/
364 print('Continue uploading? (y/N) ', end='') 382 print('Continue uploading? (y/N) ', end='')
365 # TODO: When we require Python 3, use flush=True w/print above. 383 # TODO: When we require Python 3, use flush=True w/print above.
366 sys.stdout.flush() 384 sys.stdout.flush()
367 a = sys.stdin.readline().strip().lower() 385 if opt.yes:
386 print('<--yes>')
387 a = 'yes'
388 else:
389 a = sys.stdin.readline().strip().lower()
368 if a not in ('y', 'yes', 't', 'true', 'on'): 390 if a not in ('y', 'yes', 't', 'true', 'on'):
369 print("skipping upload", file=sys.stderr) 391 print("skipping upload", file=sys.stderr)
370 branch.uploaded = False 392 branch.uploaded = False
@@ -376,12 +398,51 @@ Gerrit Code Review: https://www.gerritcodereview.com/
376 key = 'review.%s.uploadtopic' % branch.project.remote.review 398 key = 'review.%s.uploadtopic' % branch.project.remote.review
377 opt.auto_topic = branch.project.config.GetBoolean(key) 399 opt.auto_topic = branch.project.config.GetBoolean(key)
378 400
401 def _ExpandCommaList(value):
402 """Split |value| up into comma delimited entries."""
403 if not value:
404 return
405 for ret in value.split(','):
406 ret = ret.strip()
407 if ret:
408 yield ret
409
410 # Check if hashtags should be included.
411 key = 'review.%s.uploadhashtags' % branch.project.remote.review
412 hashtags = set(_ExpandCommaList(branch.project.config.GetString(key)))
413 for tag in opt.hashtags:
414 hashtags.update(_ExpandCommaList(tag))
415 if opt.hashtag_branch:
416 hashtags.add(branch.name)
417
418 # Check if labels should be included.
419 key = 'review.%s.uploadlabels' % branch.project.remote.review
420 labels = set(_ExpandCommaList(branch.project.config.GetString(key)))
421 for label in opt.labels:
422 labels.update(_ExpandCommaList(label))
423 # Basic sanity check on label syntax.
424 for label in labels:
425 if not re.match(r'^.+[+-][0-9]+$', label):
426 print('repo: error: invalid label syntax "%s": labels use forms '
427 'like CodeReview+1 or Verified-1' % (label,), file=sys.stderr)
428 sys.exit(1)
429
430 # Handle e-mail notifications.
431 if opt.notify is False:
432 notify = 'NONE'
433 else:
434 key = 'review.%s.uploadnotify' % branch.project.remote.review
435 notify = branch.project.config.GetString(key)
436
379 destination = opt.dest_branch or branch.project.dest_branch 437 destination = opt.dest_branch or branch.project.dest_branch
380 438
381 # Make sure our local branch is not setup to track a different remote branch 439 # Make sure our local branch is not setup to track a different remote branch
382 merge_branch = self._GetMergeBranch(branch.project) 440 merge_branch = self._GetMergeBranch(branch.project)
383 if destination: 441 if destination:
384 full_dest = 'refs/heads/%s' % destination 442 full_dest = destination
443 if not full_dest.startswith(R_HEADS):
444 full_dest = R_HEADS + full_dest
445
385 if not opt.dest_branch and merge_branch and merge_branch != full_dest: 446 if not opt.dest_branch and merge_branch and merge_branch != full_dest:
386 print('merge branch %s does not match destination branch %s' 447 print('merge branch %s does not match destination branch %s'
387 % (merge_branch, full_dest)) 448 % (merge_branch, full_dest))
@@ -392,10 +453,12 @@ Gerrit Code Review: https://www.gerritcodereview.com/
392 continue 453 continue
393 454
394 branch.UploadForReview(people, 455 branch.UploadForReview(people,
456 dryrun=opt.dryrun,
395 auto_topic=opt.auto_topic, 457 auto_topic=opt.auto_topic,
396 draft=opt.draft, 458 hashtags=hashtags,
459 labels=labels,
397 private=opt.private, 460 private=opt.private,
398 notify=None if opt.notify else 'NONE', 461 notify=notify,
399 wip=opt.wip, 462 wip=opt.wip,
400 dest_branch=destination, 463 dest_branch=destination,
401 validate_certs=opt.validate_certs, 464 validate_certs=opt.validate_certs,
@@ -418,18 +481,18 @@ Gerrit Code Review: https://www.gerritcodereview.com/
418 else: 481 else:
419 fmt = '\n (%s)' 482 fmt = '\n (%s)'
420 print(('[FAILED] %-15s %-15s' + fmt) % ( 483 print(('[FAILED] %-15s %-15s' + fmt) % (
421 branch.project.relpath + '/', \ 484 branch.project.relpath + '/',
422 branch.name, \ 485 branch.name,
423 str(branch.error)), 486 str(branch.error)),
424 file=sys.stderr) 487 file=sys.stderr)
425 print() 488 print()
426 489
427 for branch in todo: 490 for branch in todo:
428 if branch.uploaded: 491 if branch.uploaded:
429 print('[OK ] %-15s %s' % ( 492 print('[OK ] %-15s %s' % (
430 branch.project.relpath + '/', 493 branch.project.relpath + '/',
431 branch.name), 494 branch.name),
432 file=sys.stderr) 495 file=sys.stderr)
433 496
434 if have_errors: 497 if have_errors:
435 sys.exit(1) 498 sys.exit(1)
@@ -437,68 +500,72 @@ Gerrit Code Review: https://www.gerritcodereview.com/
437 def _GetMergeBranch(self, project): 500 def _GetMergeBranch(self, project):
438 p = GitCommand(project, 501 p = GitCommand(project,
439 ['rev-parse', '--abbrev-ref', 'HEAD'], 502 ['rev-parse', '--abbrev-ref', 'HEAD'],
440 capture_stdout = True, 503 capture_stdout=True,
441 capture_stderr = True) 504 capture_stderr=True)
442 p.Wait() 505 p.Wait()
443 local_branch = p.stdout.strip() 506 local_branch = p.stdout.strip()
444 p = GitCommand(project, 507 p = GitCommand(project,
445 ['config', '--get', 'branch.%s.merge' % local_branch], 508 ['config', '--get', 'branch.%s.merge' % local_branch],
446 capture_stdout = True, 509 capture_stdout=True,
447 capture_stderr = True) 510 capture_stderr=True)
448 p.Wait() 511 p.Wait()
449 merge_branch = p.stdout.strip() 512 merge_branch = p.stdout.strip()
450 return merge_branch 513 return merge_branch
451 514
515 @staticmethod
516 def _GatherOne(opt, project):
517 """Figure out the upload status for |project|."""
518 if opt.current_branch:
519 cbr = project.CurrentBranch
520 up_branch = project.GetUploadableBranch(cbr)
521 avail = [up_branch] if up_branch else None
522 else:
523 avail = project.GetUploadableBranches(opt.branch)
524 return (project, avail)
525
452 def Execute(self, opt, args): 526 def Execute(self, opt, args):
453 project_list = self.GetProjects(args) 527 projects = self.GetProjects(args)
454 pending = [] 528
455 reviewers = [] 529 def _ProcessResults(_pool, _out, results):
456 cc = [] 530 pending = []
457 branch = None 531 for result in results:
458 532 project, avail = result
459 if opt.branch: 533 if avail is None:
460 branch = opt.branch 534 print('repo: error: %s: Unable to upload branch "%s". '
461 535 'You might be able to fix the branch by running:\n'
462 for project in project_list: 536 ' git branch --set-upstream-to m/%s' %
463 if opt.current_branch: 537 (project.relpath, project.CurrentBranch, self.manifest.branch),
464 cbr = project.CurrentBranch
465 up_branch = project.GetUploadableBranch(cbr)
466 if up_branch:
467 avail = [up_branch]
468 else:
469 avail = None
470 print('ERROR: Current branch (%s) not uploadable. '
471 'You may be able to type '
472 '"git branch --set-upstream-to m/master" to fix '
473 'your branch.' % str(cbr),
474 file=sys.stderr) 538 file=sys.stderr)
475 else: 539 elif avail:
476 avail = project.GetUploadableBranches(branch) 540 pending.append(result)
477 if avail: 541 return pending
478 pending.append((project, avail)) 542
543 pending = self.ExecuteInParallel(
544 opt.jobs,
545 functools.partial(self._GatherOne, opt),
546 projects,
547 callback=_ProcessResults)
479 548
480 if not pending: 549 if not pending:
481 print("no branches ready for upload", file=sys.stderr) 550 if opt.branch is None:
482 return 551 print('repo: error: no branches ready for upload', file=sys.stderr)
483 552 else:
484 if not opt.bypass_hooks: 553 print('repo: error: no branches named "%s" ready for upload' %
485 hook = RepoHook('pre-upload', self.manifest.repo_hooks_project, 554 (opt.branch,), file=sys.stderr)
486 self.manifest.topdir, 555 return 1
487 self.manifest.manifestProject.GetRemote('origin').url, 556
488 abort_if_user_denies=True) 557 pending_proj_names = [project.name for (project, available) in pending]
489 pending_proj_names = [project.name for (project, available) in pending] 558 pending_worktrees = [project.worktree for (project, available) in pending]
490 pending_worktrees = [project.worktree for (project, available) in pending] 559 hook = RepoHook.FromSubcmd(
491 try: 560 hook_type='pre-upload', manifest=self.manifest,
492 hook.Run(opt.allow_all_hooks, project_list=pending_proj_names, 561 opt=opt, abort_if_user_denies=True)
493 worktree_list=pending_worktrees) 562 if not hook.Run(
494 except HookError as e: 563 project_list=pending_proj_names,
495 print("ERROR: %s" % str(e), file=sys.stderr) 564 worktree_list=pending_worktrees):
496 return 565 return 1
497 566
498 if opt.reviewers: 567 reviewers = _SplitEmails(opt.reviewers) if opt.reviewers else []
499 reviewers = _SplitEmails(opt.reviewers) 568 cc = _SplitEmails(opt.cc) if opt.cc else []
500 if opt.cc:
501 cc = _SplitEmails(opt.cc)
502 people = (reviewers, cc) 569 people = (reviewers, cc)
503 570
504 if len(pending) == 1 and len(pending[0][1]) == 1: 571 if len(pending) == 1 and len(pending[0][1]) == 1:
diff --git a/subcmds/version.py b/subcmds/version.py
index 761172b7..09b053ea 100644
--- a/subcmds/version.py
+++ b/subcmds/version.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2009 The Android Open Source Project 1# Copyright (C) 2009 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,17 +12,20 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function 15import platform
18import sys 16import sys
17
19from command import Command, MirrorSafeCommand 18from command import Command, MirrorSafeCommand
20from git_command import git, RepoSourceVersion, user_agent 19from git_command import git, RepoSourceVersion, user_agent
21from git_refs import HEAD 20from git_refs import HEAD
21from wrapper import Wrapper
22
22 23
23class Version(Command, MirrorSafeCommand): 24class Version(Command, MirrorSafeCommand):
24 wrapper_version = None 25 wrapper_version = None
25 wrapper_path = None 26 wrapper_path = None
26 27
27 common = False 28 COMMON = False
28 helpSummary = "Display the version of repo" 29 helpSummary = "Display the version of repo"
29 helpUsage = """ 30 helpUsage = """
30%prog 31%prog
@@ -33,16 +34,19 @@ class Version(Command, MirrorSafeCommand):
33 def Execute(self, opt, args): 34 def Execute(self, opt, args):
34 rp = self.manifest.repoProject 35 rp = self.manifest.repoProject
35 rem = rp.GetRemote(rp.remote.name) 36 rem = rp.GetRemote(rp.remote.name)
37 branch = rp.GetBranch('default')
36 38
37 # These might not be the same. Report them both. 39 # These might not be the same. Report them both.
38 src_ver = RepoSourceVersion() 40 src_ver = RepoSourceVersion()
39 rp_ver = rp.bare_git.describe(HEAD) 41 rp_ver = rp.bare_git.describe(HEAD)
40 print('repo version %s' % rp_ver) 42 print('repo version %s' % rp_ver)
41 print(' (from %s)' % rem.url) 43 print(' (from %s)' % rem.url)
44 print(' (tracking %s)' % branch.merge)
45 print(' (%s)' % rp.bare_git.log('-1', '--format=%cD', HEAD))
42 46
43 if Version.wrapper_path is not None: 47 if self.wrapper_path is not None:
44 print('repo launcher version %s' % Version.wrapper_version) 48 print('repo launcher version %s' % self.wrapper_version)
45 print(' (from %s)' % Version.wrapper_path) 49 print(' (from %s)' % self.wrapper_path)
46 50
47 if src_ver != rp_ver: 51 if src_ver != rp_ver:
48 print(' (currently at %s)' % src_ver) 52 print(' (currently at %s)' % src_ver)
@@ -51,3 +55,12 @@ class Version(Command, MirrorSafeCommand):
51 print('git %s' % git.version_tuple().full) 55 print('git %s' % git.version_tuple().full)
52 print('git User-Agent %s' % user_agent.git) 56 print('git User-Agent %s' % user_agent.git)
53 print('Python %s' % sys.version) 57 print('Python %s' % sys.version)
58 uname = platform.uname()
59 if sys.version_info.major < 3:
60 # Python 3 returns a named tuple, but Python 2 is simpler.
61 print(uname)
62 else:
63 print('OS %s %s (%s)' % (uname.system, uname.release, uname.version))
64 print('CPU %s (%s)' %
65 (uname.machine, uname.processor if uname.processor else 'unknown'))
66 print('Bug reports:', Wrapper().BUG_URL)
diff --git a/tests/fixtures/test.gitconfig b/tests/fixtures/test.gitconfig
index 3c573c9e..b178cf60 100644
--- a/tests/fixtures/test.gitconfig
+++ b/tests/fixtures/test.gitconfig
@@ -1,3 +1,23 @@
1[section] 1[section]
2 empty 2 empty
3 nonempty = true 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[repo "syncstate.main"]
15 synctime = 2021-09-14T17:23:43.537338Z
16 version = 1
17[repo "syncstate.sys"]
18 argv = ['/usr/bin/pytest-3']
19[repo "syncstate.superproject"]
20 test = false
21[repo "syncstate.options"]
22 verbose = true
23 mpupdate = false
diff --git a/tests/test_editor.py b/tests/test_editor.py
index fbcfcdbd..cfd4f5ed 100644
--- a/tests/test_editor.py
+++ b/tests/test_editor.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2019 The Android Open Source Project 1# Copyright (C) 2019 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,8 +14,6 @@
16 14
17"""Unittests for the editor.py module.""" 15"""Unittests for the editor.py module."""
18 16
19from __future__ import print_function
20
21import unittest 17import unittest
22 18
23from editor import Editor 19from editor import Editor
diff --git a/tests/test_error.py b/tests/test_error.py
new file mode 100644
index 00000000..82b00c24
--- /dev/null
+++ b/tests/test_error.py
@@ -0,0 +1,53 @@
1# Copyright 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
17import inspect
18import pickle
19import unittest
20
21import error
22
23
24class PickleTests(unittest.TestCase):
25 """Make sure all our custom exceptions can be pickled."""
26
27 def getExceptions(self):
28 """Return all our custom exceptions."""
29 for name in dir(error):
30 cls = getattr(error, name)
31 if isinstance(cls, type) and issubclass(cls, Exception):
32 yield cls
33
34 def testExceptionLookup(self):
35 """Make sure our introspection logic works."""
36 classes = list(self.getExceptions())
37 self.assertIn(error.HookError, classes)
38 # Don't assert the exact number to avoid being a change-detector test.
39 self.assertGreater(len(classes), 10)
40
41 def testPickle(self):
42 """Try to pickle all the exceptions."""
43 for cls in self.getExceptions():
44 args = inspect.getfullargspec(cls.__init__).args[1:]
45 obj = cls(*args)
46 p = pickle.dumps(obj)
47 try:
48 newobj = pickle.loads(p)
49 except Exception as e: # pylint: disable=broad-except
50 self.fail('Class %s is unable to be pickled: %s\n'
51 'Incomplete super().__init__(...) call?' % (cls, e))
52 self.assertIsInstance(newobj, cls)
53 self.assertEqual(str(obj), str(newobj))
diff --git a/tests/test_git_command.py b/tests/test_git_command.py
index 51171a32..93300a6f 100644
--- a/tests/test_git_command.py
+++ b/tests/test_git_command.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright 2019 The Android Open Source Project 1# Copyright 2019 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,12 +14,16 @@
16 14
17"""Unittests for the git_command.py module.""" 15"""Unittests for the git_command.py module."""
18 16
19from __future__ import print_function
20
21import re 17import re
22import unittest 18import unittest
23 19
20try:
21 from unittest import mock
22except ImportError:
23 import mock
24
24import git_command 25import git_command
26import wrapper
25 27
26 28
27class GitCallUnitTest(unittest.TestCase): 29class GitCallUnitTest(unittest.TestCase):
@@ -35,7 +37,7 @@ class GitCallUnitTest(unittest.TestCase):
35 # We don't dive too deep into the values here to avoid having to update 37 # We don't dive too deep into the values here to avoid having to update
36 # whenever git versions change. We do check relative to this min version 38 # whenever git versions change. We do check relative to this min version
37 # as this is what `repo` itself requires via MIN_GIT_VERSION. 39 # as this is what `repo` itself requires via MIN_GIT_VERSION.
38 MIN_GIT_VERSION = (1, 7, 2) 40 MIN_GIT_VERSION = (2, 10, 2)
39 self.assertTrue(isinstance(ver.major, int)) 41 self.assertTrue(isinstance(ver.major, int))
40 self.assertTrue(isinstance(ver.minor, int)) 42 self.assertTrue(isinstance(ver.minor, int))
41 self.assertTrue(isinstance(ver.micro, int)) 43 self.assertTrue(isinstance(ver.micro, int))
@@ -76,3 +78,45 @@ class UserAgentUnitTest(unittest.TestCase):
76 # the general form. 78 # the general form.
77 m = re.match(r'^git/[^ ]+ ([^ ]+) git-repo/[^ ]+', ua) 79 m = re.match(r'^git/[^ ]+ ([^ ]+) git-repo/[^ ]+', ua)
78 self.assertIsNotNone(m) 80 self.assertIsNotNone(m)
81
82
83class GitRequireTests(unittest.TestCase):
84 """Test the git_require helper."""
85
86 def setUp(self):
87 ver = wrapper.GitVersion(1, 2, 3, 4)
88 mock.patch.object(git_command.git, 'version_tuple', return_value=ver).start()
89
90 def tearDown(self):
91 mock.patch.stopall()
92
93 def test_older_nonfatal(self):
94 """Test non-fatal require calls with old versions."""
95 self.assertFalse(git_command.git_require((2,)))
96 self.assertFalse(git_command.git_require((1, 3)))
97 self.assertFalse(git_command.git_require((1, 2, 4)))
98 self.assertFalse(git_command.git_require((1, 2, 3, 5)))
99
100 def test_newer_nonfatal(self):
101 """Test non-fatal require calls with newer versions."""
102 self.assertTrue(git_command.git_require((0,)))
103 self.assertTrue(git_command.git_require((1, 0)))
104 self.assertTrue(git_command.git_require((1, 2, 0)))
105 self.assertTrue(git_command.git_require((1, 2, 3, 0)))
106
107 def test_equal_nonfatal(self):
108 """Test require calls with equal values."""
109 self.assertTrue(git_command.git_require((1, 2, 3, 4), fail=False))
110 self.assertTrue(git_command.git_require((1, 2, 3, 4), fail=True))
111
112 def test_older_fatal(self):
113 """Test fatal require calls with old versions."""
114 with self.assertRaises(SystemExit) as e:
115 git_command.git_require((2,), fail=True)
116 self.assertNotEqual(0, e.code)
117
118 def test_older_fatal_msg(self):
119 """Test fatal require calls with old versions and message."""
120 with self.assertRaises(SystemExit) as e:
121 git_command.git_require((2,), fail=True, msg='so sad')
122 self.assertNotEqual(0, e.code)
diff --git a/tests/test_git_config.py b/tests/test_git_config.py
index b735f27f..faf12a2e 100644
--- a/tests/test_git_config.py
+++ b/tests/test_git_config.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2009 The Android Open Source Project 1# Copyright (C) 2009 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,21 +14,22 @@
16 14
17"""Unittests for the git_config.py module.""" 15"""Unittests for the git_config.py module."""
18 16
19from __future__ import print_function
20
21import os 17import os
18import tempfile
22import unittest 19import unittest
23 20
24import git_config 21import git_config
25 22
23
26def fixture(*paths): 24def fixture(*paths):
27 """Return a path relative to test/fixtures. 25 """Return a path relative to test/fixtures.
28 """ 26 """
29 return os.path.join(os.path.dirname(__file__), 'fixtures', *paths) 27 return os.path.join(os.path.dirname(__file__), 'fixtures', *paths)
30 28
31class GitConfigUnitTest(unittest.TestCase): 29
32 """Tests the GitConfig class. 30class GitConfigReadOnlyTests(unittest.TestCase):
33 """ 31 """Read-only tests of the GitConfig class."""
32
34 def setUp(self): 33 def setUp(self):
35 """Create a GitConfig object using the test.gitconfig fixture. 34 """Create a GitConfig object using the test.gitconfig fixture.
36 """ 35 """
@@ -68,5 +67,126 @@ class GitConfigUnitTest(unittest.TestCase):
68 val = config.GetString('empty') 67 val = config.GetString('empty')
69 self.assertEqual(val, None) 68 self.assertEqual(val, None)
70 69
70 def test_GetBoolean_undefined(self):
71 """Test GetBoolean on key that doesn't exist."""
72 self.assertIsNone(self.config.GetBoolean('section.missing'))
73
74 def test_GetBoolean_invalid(self):
75 """Test GetBoolean on invalid boolean value."""
76 self.assertIsNone(self.config.GetBoolean('section.boolinvalid'))
77
78 def test_GetBoolean_true(self):
79 """Test GetBoolean on valid true boolean."""
80 self.assertTrue(self.config.GetBoolean('section.booltrue'))
81
82 def test_GetBoolean_false(self):
83 """Test GetBoolean on valid false boolean."""
84 self.assertFalse(self.config.GetBoolean('section.boolfalse'))
85
86 def test_GetInt_undefined(self):
87 """Test GetInt on key that doesn't exist."""
88 self.assertIsNone(self.config.GetInt('section.missing'))
89
90 def test_GetInt_invalid(self):
91 """Test GetInt on invalid integer value."""
92 self.assertIsNone(self.config.GetBoolean('section.intinvalid'))
93
94 def test_GetInt_valid(self):
95 """Test GetInt on valid integers."""
96 TESTS = (
97 ('inthex', 16),
98 ('inthexk', 16384),
99 ('int', 10),
100 ('intk', 10240),
101 ('intm', 10485760),
102 ('intg', 10737418240),
103 )
104 for key, value in TESTS:
105 self.assertEqual(value, self.config.GetInt('section.%s' % (key,)))
106
107 def test_GetSyncAnalysisStateData(self):
108 """Test config entries with a sync state analysis data."""
109 superproject_logging_data = {}
110 superproject_logging_data['test'] = False
111 options = type('options', (object,), {})()
112 options.verbose = 'true'
113 options.mp_update = 'false'
114 TESTS = (
115 ('superproject.test', 'false'),
116 ('options.verbose', 'true'),
117 ('options.mpupdate', 'false'),
118 ('main.version', '1'),
119 )
120 self.config.UpdateSyncAnalysisState(options, superproject_logging_data)
121 sync_data = self.config.GetSyncAnalysisStateData()
122 for key, value in TESTS:
123 self.assertEqual(sync_data[f'{git_config.SYNC_STATE_PREFIX}{key}'], value)
124 self.assertTrue(sync_data[f'{git_config.SYNC_STATE_PREFIX}main.synctime'])
125
126
127class GitConfigReadWriteTests(unittest.TestCase):
128 """Read/write tests of the GitConfig class."""
129
130 def setUp(self):
131 self.tmpfile = tempfile.NamedTemporaryFile()
132 self.config = self.get_config()
133
134 def get_config(self):
135 """Get a new GitConfig instance."""
136 return git_config.GitConfig(self.tmpfile.name)
137
138 def test_SetString(self):
139 """Test SetString behavior."""
140 # Set a value.
141 self.assertIsNone(self.config.GetString('foo.bar'))
142 self.config.SetString('foo.bar', 'val')
143 self.assertEqual('val', self.config.GetString('foo.bar'))
144
145 # Make sure the value was actually written out.
146 config = self.get_config()
147 self.assertEqual('val', config.GetString('foo.bar'))
148
149 # Update the value.
150 self.config.SetString('foo.bar', 'valll')
151 self.assertEqual('valll', self.config.GetString('foo.bar'))
152 config = self.get_config()
153 self.assertEqual('valll', config.GetString('foo.bar'))
154
155 # Delete the value.
156 self.config.SetString('foo.bar', None)
157 self.assertIsNone(self.config.GetString('foo.bar'))
158 config = self.get_config()
159 self.assertIsNone(config.GetString('foo.bar'))
160
161 def test_SetBoolean(self):
162 """Test SetBoolean behavior."""
163 # Set a true value.
164 self.assertIsNone(self.config.GetBoolean('foo.bar'))
165 for val in (True, 1):
166 self.config.SetBoolean('foo.bar', val)
167 self.assertTrue(self.config.GetBoolean('foo.bar'))
168
169 # Make sure the value was actually written out.
170 config = self.get_config()
171 self.assertTrue(config.GetBoolean('foo.bar'))
172 self.assertEqual('true', config.GetString('foo.bar'))
173
174 # Set a false value.
175 for val in (False, 0):
176 self.config.SetBoolean('foo.bar', val)
177 self.assertFalse(self.config.GetBoolean('foo.bar'))
178
179 # Make sure the value was actually written out.
180 config = self.get_config()
181 self.assertFalse(config.GetBoolean('foo.bar'))
182 self.assertEqual('false', config.GetString('foo.bar'))
183
184 # Delete the value.
185 self.config.SetBoolean('foo.bar', None)
186 self.assertIsNone(self.config.GetBoolean('foo.bar'))
187 config = self.get_config()
188 self.assertIsNone(config.GetBoolean('foo.bar'))
189
190
71if __name__ == '__main__': 191if __name__ == '__main__':
72 unittest.main() 192 unittest.main()
diff --git a/tests/test_git_superproject.py b/tests/test_git_superproject.py
new file mode 100644
index 00000000..a24fc7f0
--- /dev/null
+++ b/tests/test_git_superproject.py
@@ -0,0 +1,376 @@
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 git_superproject.py module."""
16
17import json
18import os
19import platform
20import tempfile
21import unittest
22from unittest import mock
23
24import git_superproject
25import git_trace2_event_log
26import manifest_xml
27import platform_utils
28from test_manifest_xml import sort_attributes
29
30
31class SuperprojectTestCase(unittest.TestCase):
32 """TestCase for the Superproject module."""
33
34 PARENT_SID_KEY = 'GIT_TRACE2_PARENT_SID'
35 PARENT_SID_VALUE = 'parent_sid'
36 SELF_SID_REGEX = r'repo-\d+T\d+Z-.*'
37 FULL_SID_REGEX = r'^%s/%s' % (PARENT_SID_VALUE, SELF_SID_REGEX)
38
39 def setUp(self):
40 """Set up superproject every time."""
41 self.tempdir = tempfile.mkdtemp(prefix='repo_tests')
42 self.repodir = os.path.join(self.tempdir, '.repo')
43 self.manifest_file = os.path.join(
44 self.repodir, manifest_xml.MANIFEST_FILE_NAME)
45 os.mkdir(self.repodir)
46 self.platform = platform.system().lower()
47
48 # By default we initialize with the expected case where
49 # repo launches us (so GIT_TRACE2_PARENT_SID is set).
50 env = {
51 self.PARENT_SID_KEY: self.PARENT_SID_VALUE,
52 }
53 self.git_event_log = git_trace2_event_log.EventLog(env=env)
54
55 # The manifest parsing really wants a git repo currently.
56 gitdir = os.path.join(self.repodir, 'manifests.git')
57 os.mkdir(gitdir)
58 with open(os.path.join(gitdir, 'config'), 'w') as fp:
59 fp.write("""[remote "origin"]
60 url = https://localhost:0/manifest
61""")
62
63 manifest = self.getXmlManifest("""
64<manifest>
65 <remote name="default-remote" fetch="http://localhost" />
66 <default remote="default-remote" revision="refs/heads/main" />
67 <superproject name="superproject"/>
68 <project path="art" name="platform/art" groups="notdefault,platform-""" + self.platform + """
69 " /></manifest>
70""")
71 self._superproject = git_superproject.Superproject(manifest, self.repodir,
72 self.git_event_log)
73
74 def tearDown(self):
75 """Tear down superproject every time."""
76 platform_utils.rmtree(self.tempdir)
77
78 def getXmlManifest(self, data):
79 """Helper to initialize a manifest for testing."""
80 with open(self.manifest_file, 'w') as fp:
81 fp.write(data)
82 return manifest_xml.XmlManifest(self.repodir, self.manifest_file)
83
84 def verifyCommonKeys(self, log_entry, expected_event_name, full_sid=True):
85 """Helper function to verify common event log keys."""
86 self.assertIn('event', log_entry)
87 self.assertIn('sid', log_entry)
88 self.assertIn('thread', log_entry)
89 self.assertIn('time', log_entry)
90
91 # Do basic data format validation.
92 self.assertEqual(expected_event_name, log_entry['event'])
93 if full_sid:
94 self.assertRegex(log_entry['sid'], self.FULL_SID_REGEX)
95 else:
96 self.assertRegex(log_entry['sid'], self.SELF_SID_REGEX)
97 self.assertRegex(log_entry['time'], r'^\d+-\d+-\d+T\d+:\d+:\d+\.\d+Z$')
98
99 def readLog(self, log_path):
100 """Helper function to read log data into a list."""
101 log_data = []
102 with open(log_path, mode='rb') as f:
103 for line in f:
104 log_data.append(json.loads(line))
105 return log_data
106
107 def verifyErrorEvent(self):
108 """Helper to verify that error event is written."""
109
110 with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
111 log_path = self.git_event_log.Write(path=tempdir)
112 self.log_data = self.readLog(log_path)
113
114 self.assertEqual(len(self.log_data), 2)
115 error_event = self.log_data[1]
116 self.verifyCommonKeys(self.log_data[0], expected_event_name='version')
117 self.verifyCommonKeys(error_event, expected_event_name='error')
118 # Check for 'error' event specific fields.
119 self.assertIn('msg', error_event)
120 self.assertIn('fmt', error_event)
121
122 def test_superproject_get_superproject_no_superproject(self):
123 """Test with no url."""
124 manifest = self.getXmlManifest("""
125<manifest>
126</manifest>
127""")
128 superproject = git_superproject.Superproject(manifest, self.repodir, self.git_event_log)
129 # Test that exit condition is false when there is no superproject tag.
130 sync_result = superproject.Sync()
131 self.assertFalse(sync_result.success)
132 self.assertFalse(sync_result.fatal)
133 self.verifyErrorEvent()
134
135 def test_superproject_get_superproject_invalid_url(self):
136 """Test with an invalid url."""
137 manifest = self.getXmlManifest("""
138<manifest>
139 <remote name="test-remote" fetch="localhost" />
140 <default remote="test-remote" revision="refs/heads/main" />
141 <superproject name="superproject"/>
142</manifest>
143""")
144 superproject = git_superproject.Superproject(manifest, self.repodir, self.git_event_log)
145 sync_result = superproject.Sync()
146 self.assertFalse(sync_result.success)
147 self.assertTrue(sync_result.fatal)
148
149 def test_superproject_get_superproject_invalid_branch(self):
150 """Test with an invalid branch."""
151 manifest = self.getXmlManifest("""
152<manifest>
153 <remote name="test-remote" fetch="localhost" />
154 <default remote="test-remote" revision="refs/heads/main" />
155 <superproject name="superproject"/>
156</manifest>
157""")
158 self._superproject = git_superproject.Superproject(manifest, self.repodir,
159 self.git_event_log)
160 with mock.patch.object(self._superproject, '_branch', 'junk'):
161 sync_result = self._superproject.Sync()
162 self.assertFalse(sync_result.success)
163 self.assertTrue(sync_result.fatal)
164
165 def test_superproject_get_superproject_mock_init(self):
166 """Test with _Init failing."""
167 with mock.patch.object(self._superproject, '_Init', return_value=False):
168 sync_result = self._superproject.Sync()
169 self.assertFalse(sync_result.success)
170 self.assertTrue(sync_result.fatal)
171
172 def test_superproject_get_superproject_mock_fetch(self):
173 """Test with _Fetch failing."""
174 with mock.patch.object(self._superproject, '_Init', return_value=True):
175 os.mkdir(self._superproject._superproject_path)
176 with mock.patch.object(self._superproject, '_Fetch', return_value=False):
177 sync_result = self._superproject.Sync()
178 self.assertFalse(sync_result.success)
179 self.assertTrue(sync_result.fatal)
180
181 def test_superproject_get_all_project_commit_ids_mock_ls_tree(self):
182 """Test with LsTree being a mock."""
183 data = ('120000 blob 158258bdf146f159218e2b90f8b699c4d85b5804\tAndroid.bp\x00'
184 '160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00'
185 '160000 commit e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06\tbootable/recovery\x00'
186 '120000 blob acc2cbdf438f9d2141f0ae424cec1d8fc4b5d97f\tbootstrap.bash\x00'
187 '160000 commit ade9b7a0d874e25fff4bf2552488825c6f111928\tbuild/bazel\x00')
188 with mock.patch.object(self._superproject, '_Init', return_value=True):
189 with mock.patch.object(self._superproject, '_Fetch', return_value=True):
190 with mock.patch.object(self._superproject, '_LsTree', return_value=data):
191 commit_ids_result = self._superproject._GetAllProjectsCommitIds()
192 self.assertEqual(commit_ids_result.commit_ids, {
193 'art': '2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea',
194 'bootable/recovery': 'e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06',
195 'build/bazel': 'ade9b7a0d874e25fff4bf2552488825c6f111928'
196 })
197 self.assertFalse(commit_ids_result.fatal)
198
199 def test_superproject_write_manifest_file(self):
200 """Test with writing manifest to a file after setting revisionId."""
201 self.assertEqual(len(self._superproject._manifest.projects), 1)
202 project = self._superproject._manifest.projects[0]
203 project.SetRevisionId('ABCDEF')
204 # Create temporary directory so that it can write the file.
205 os.mkdir(self._superproject._superproject_path)
206 manifest_path = self._superproject._WriteManifestFile()
207 self.assertIsNotNone(manifest_path)
208 with open(manifest_path, 'r') as fp:
209 manifest_xml_data = fp.read()
210 self.assertEqual(
211 sort_attributes(manifest_xml_data),
212 '<?xml version="1.0" ?><manifest>'
213 '<remote fetch="http://localhost" name="default-remote"/>'
214 '<default remote="default-remote" revision="refs/heads/main"/>'
215 '<project groups="notdefault,platform-' + self.platform + '" '
216 'name="platform/art" path="art" revision="ABCDEF" upstream="refs/heads/main"/>'
217 '<superproject name="superproject"/>'
218 '</manifest>')
219
220 def test_superproject_update_project_revision_id(self):
221 """Test with LsTree being a mock."""
222 self.assertEqual(len(self._superproject._manifest.projects), 1)
223 projects = self._superproject._manifest.projects
224 data = ('160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00'
225 '160000 commit e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06\tbootable/recovery\x00')
226 with mock.patch.object(self._superproject, '_Init', return_value=True):
227 with mock.patch.object(self._superproject, '_Fetch', return_value=True):
228 with mock.patch.object(self._superproject,
229 '_LsTree',
230 return_value=data):
231 # Create temporary directory so that it can write the file.
232 os.mkdir(self._superproject._superproject_path)
233 update_result = self._superproject.UpdateProjectsRevisionId(projects)
234 self.assertIsNotNone(update_result.manifest_path)
235 self.assertFalse(update_result.fatal)
236 with open(update_result.manifest_path, 'r') as fp:
237 manifest_xml_data = fp.read()
238 self.assertEqual(
239 sort_attributes(manifest_xml_data),
240 '<?xml version="1.0" ?><manifest>'
241 '<remote fetch="http://localhost" name="default-remote"/>'
242 '<default remote="default-remote" revision="refs/heads/main"/>'
243 '<project groups="notdefault,platform-' + self.platform + '" '
244 'name="platform/art" path="art" '
245 'revision="2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea" upstream="refs/heads/main"/>'
246 '<superproject name="superproject"/>'
247 '</manifest>')
248
249 def test_superproject_update_project_revision_id_no_superproject_tag(self):
250 """Test update of commit ids of a manifest without superproject tag."""
251 manifest = self.getXmlManifest("""
252<manifest>
253 <remote name="default-remote" fetch="http://localhost" />
254 <default remote="default-remote" revision="refs/heads/main" />
255 <project name="test-name"/>
256</manifest>
257""")
258 self.maxDiff = None
259 self._superproject = git_superproject.Superproject(manifest, self.repodir,
260 self.git_event_log)
261 self.assertEqual(len(self._superproject._manifest.projects), 1)
262 projects = self._superproject._manifest.projects
263 project = projects[0]
264 project.SetRevisionId('ABCDEF')
265 update_result = self._superproject.UpdateProjectsRevisionId(projects)
266 self.assertIsNone(update_result.manifest_path)
267 self.assertFalse(update_result.fatal)
268 self.verifyErrorEvent()
269 self.assertEqual(
270 sort_attributes(manifest.ToXml().toxml()),
271 '<?xml version="1.0" ?><manifest>'
272 '<remote fetch="http://localhost" name="default-remote"/>'
273 '<default remote="default-remote" revision="refs/heads/main"/>'
274 '<project name="test-name" revision="ABCDEF" upstream="refs/heads/main"/>'
275 '</manifest>')
276
277 def test_superproject_update_project_revision_id_from_local_manifest_group(self):
278 """Test update of commit ids of a manifest that have local manifest no superproject group."""
279 local_group = manifest_xml.LOCAL_MANIFEST_GROUP_PREFIX + ':local'
280 manifest = self.getXmlManifest("""
281<manifest>
282 <remote name="default-remote" fetch="http://localhost" />
283 <remote name="goog" fetch="http://localhost2" />
284 <default remote="default-remote" revision="refs/heads/main" />
285 <superproject name="superproject"/>
286 <project path="vendor/x" name="platform/vendor/x" remote="goog"
287 groups=\"""" + local_group + """
288 " revision="master-with-vendor" clone-depth="1" />
289 <project path="art" name="platform/art" groups="notdefault,platform-""" + self.platform + """
290 " /></manifest>
291""")
292 self.maxDiff = None
293 self._superproject = git_superproject.Superproject(manifest, self.repodir,
294 self.git_event_log)
295 self.assertEqual(len(self._superproject._manifest.projects), 2)
296 projects = self._superproject._manifest.projects
297 data = ('160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00')
298 with mock.patch.object(self._superproject, '_Init', return_value=True):
299 with mock.patch.object(self._superproject, '_Fetch', return_value=True):
300 with mock.patch.object(self._superproject,
301 '_LsTree',
302 return_value=data):
303 # Create temporary directory so that it can write the file.
304 os.mkdir(self._superproject._superproject_path)
305 update_result = self._superproject.UpdateProjectsRevisionId(projects)
306 self.assertIsNotNone(update_result.manifest_path)
307 self.assertFalse(update_result.fatal)
308 with open(update_result.manifest_path, 'r') as fp:
309 manifest_xml_data = fp.read()
310 # Verify platform/vendor/x's project revision hasn't changed.
311 self.assertEqual(
312 sort_attributes(manifest_xml_data),
313 '<?xml version="1.0" ?><manifest>'
314 '<remote fetch="http://localhost" name="default-remote"/>'
315 '<remote fetch="http://localhost2" name="goog"/>'
316 '<default remote="default-remote" revision="refs/heads/main"/>'
317 '<project groups="notdefault,platform-' + self.platform + '" '
318 'name="platform/art" path="art" '
319 'revision="2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea" upstream="refs/heads/main"/>'
320 '<project clone-depth="1" groups="' + local_group + '" '
321 'name="platform/vendor/x" path="vendor/x" remote="goog" '
322 'revision="master-with-vendor"/>'
323 '<superproject name="superproject"/>'
324 '</manifest>')
325
326 def test_superproject_update_project_revision_id_with_pinned_manifest(self):
327 """Test update of commit ids of a pinned manifest."""
328 manifest = self.getXmlManifest("""
329<manifest>
330 <remote name="default-remote" fetch="http://localhost" />
331 <default remote="default-remote" revision="refs/heads/main" />
332 <superproject name="superproject"/>
333 <project path="vendor/x" name="platform/vendor/x" revision="" />
334 <project path="vendor/y" name="platform/vendor/y"
335 revision="52d3c9f7c107839ece2319d077de0cd922aa9d8f" />
336 <project path="art" name="platform/art" groups="notdefault,platform-""" + self.platform + """
337 " /></manifest>
338""")
339 self.maxDiff = None
340 self._superproject = git_superproject.Superproject(manifest, self.repodir,
341 self.git_event_log)
342 self.assertEqual(len(self._superproject._manifest.projects), 3)
343 projects = self._superproject._manifest.projects
344 data = ('160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00'
345 '160000 commit e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06\tvendor/x\x00')
346 with mock.patch.object(self._superproject, '_Init', return_value=True):
347 with mock.patch.object(self._superproject, '_Fetch', return_value=True):
348 with mock.patch.object(self._superproject,
349 '_LsTree',
350 return_value=data):
351 # Create temporary directory so that it can write the file.
352 os.mkdir(self._superproject._superproject_path)
353 update_result = self._superproject.UpdateProjectsRevisionId(projects)
354 self.assertIsNotNone(update_result.manifest_path)
355 self.assertFalse(update_result.fatal)
356 with open(update_result.manifest_path, 'r') as fp:
357 manifest_xml_data = fp.read()
358 # Verify platform/vendor/x's project revision hasn't changed.
359 self.assertEqual(
360 sort_attributes(manifest_xml_data),
361 '<?xml version="1.0" ?><manifest>'
362 '<remote fetch="http://localhost" name="default-remote"/>'
363 '<default remote="default-remote" revision="refs/heads/main"/>'
364 '<project groups="notdefault,platform-' + self.platform + '" '
365 'name="platform/art" path="art" '
366 'revision="2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea" upstream="refs/heads/main"/>'
367 '<project name="platform/vendor/x" path="vendor/x" '
368 'revision="e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06" upstream="refs/heads/main"/>'
369 '<project name="platform/vendor/y" path="vendor/y" '
370 'revision="52d3c9f7c107839ece2319d077de0cd922aa9d8f"/>'
371 '<superproject name="superproject"/>'
372 '</manifest>')
373
374
375if __name__ == '__main__':
376 unittest.main()
diff --git a/tests/test_git_trace2_event_log.py b/tests/test_git_trace2_event_log.py
new file mode 100644
index 00000000..89dcfb92
--- /dev/null
+++ b/tests/test_git_trace2_event_log.py
@@ -0,0 +1,329 @@
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 git_trace2_event_log.py module."""
16
17import json
18import os
19import tempfile
20import unittest
21from unittest import mock
22
23import git_trace2_event_log
24
25
26class EventLogTestCase(unittest.TestCase):
27 """TestCase for the EventLog module."""
28
29 PARENT_SID_KEY = 'GIT_TRACE2_PARENT_SID'
30 PARENT_SID_VALUE = 'parent_sid'
31 SELF_SID_REGEX = r'repo-\d+T\d+Z-.*'
32 FULL_SID_REGEX = r'^%s/%s' % (PARENT_SID_VALUE, SELF_SID_REGEX)
33
34 def setUp(self):
35 """Load the event_log module every time."""
36 self._event_log_module = None
37 # By default we initialize with the expected case where
38 # repo launches us (so GIT_TRACE2_PARENT_SID is set).
39 env = {
40 self.PARENT_SID_KEY: self.PARENT_SID_VALUE,
41 }
42 self._event_log_module = git_trace2_event_log.EventLog(env=env)
43 self._log_data = None
44
45 def verifyCommonKeys(self, log_entry, expected_event_name=None, full_sid=True):
46 """Helper function to verify common event log keys."""
47 self.assertIn('event', log_entry)
48 self.assertIn('sid', log_entry)
49 self.assertIn('thread', log_entry)
50 self.assertIn('time', log_entry)
51
52 # Do basic data format validation.
53 if expected_event_name:
54 self.assertEqual(expected_event_name, log_entry['event'])
55 if full_sid:
56 self.assertRegex(log_entry['sid'], self.FULL_SID_REGEX)
57 else:
58 self.assertRegex(log_entry['sid'], self.SELF_SID_REGEX)
59 self.assertRegex(log_entry['time'], r'^\d+-\d+-\d+T\d+:\d+:\d+\.\d+Z$')
60
61 def readLog(self, log_path):
62 """Helper function to read log data into a list."""
63 log_data = []
64 with open(log_path, mode='rb') as f:
65 for line in f:
66 log_data.append(json.loads(line))
67 return log_data
68
69 def remove_prefix(self, s, prefix):
70 """Return a copy string after removing |prefix| from |s|, if present or the original string."""
71 if s.startswith(prefix):
72 return s[len(prefix):]
73 else:
74 return s
75
76 def test_initial_state_with_parent_sid(self):
77 """Test initial state when 'GIT_TRACE2_PARENT_SID' is set by parent."""
78 self.assertRegex(self._event_log_module.full_sid, self.FULL_SID_REGEX)
79
80 def test_initial_state_no_parent_sid(self):
81 """Test initial state when 'GIT_TRACE2_PARENT_SID' is not set."""
82 # Setup an empty environment dict (no parent sid).
83 self._event_log_module = git_trace2_event_log.EventLog(env={})
84 self.assertRegex(self._event_log_module.full_sid, self.SELF_SID_REGEX)
85
86 def test_version_event(self):
87 """Test 'version' event data is valid.
88
89 Verify that the 'version' event is written even when no other
90 events are addded.
91
92 Expected event log:
93 <version event>
94 """
95 with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
96 log_path = self._event_log_module.Write(path=tempdir)
97 self._log_data = self.readLog(log_path)
98
99 # A log with no added events should only have the version entry.
100 self.assertEqual(len(self._log_data), 1)
101 version_event = self._log_data[0]
102 self.verifyCommonKeys(version_event, expected_event_name='version')
103 # Check for 'version' event specific fields.
104 self.assertIn('evt', version_event)
105 self.assertIn('exe', version_event)
106 # Verify "evt" version field is a string.
107 self.assertIsInstance(version_event['evt'], str)
108
109 def test_start_event(self):
110 """Test and validate 'start' event data is valid.
111
112 Expected event log:
113 <version event>
114 <start event>
115 """
116 self._event_log_module.StartEvent()
117 with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
118 log_path = self._event_log_module.Write(path=tempdir)
119 self._log_data = self.readLog(log_path)
120
121 self.assertEqual(len(self._log_data), 2)
122 start_event = self._log_data[1]
123 self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
124 self.verifyCommonKeys(start_event, expected_event_name='start')
125 # Check for 'start' event specific fields.
126 self.assertIn('argv', start_event)
127 self.assertTrue(isinstance(start_event['argv'], list))
128
129 def test_exit_event_result_none(self):
130 """Test 'exit' event data is valid when result is None.
131
132 We expect None result to be converted to 0 in the exit event data.
133
134 Expected event log:
135 <version event>
136 <exit event>
137 """
138 self._event_log_module.ExitEvent(None)
139 with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
140 log_path = self._event_log_module.Write(path=tempdir)
141 self._log_data = self.readLog(log_path)
142
143 self.assertEqual(len(self._log_data), 2)
144 exit_event = self._log_data[1]
145 self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
146 self.verifyCommonKeys(exit_event, expected_event_name='exit')
147 # Check for 'exit' event specific fields.
148 self.assertIn('code', exit_event)
149 # 'None' result should convert to 0 (successful) return code.
150 self.assertEqual(exit_event['code'], 0)
151
152 def test_exit_event_result_integer(self):
153 """Test 'exit' event data is valid when result is an integer.
154
155 Expected event log:
156 <version event>
157 <exit event>
158 """
159 self._event_log_module.ExitEvent(2)
160 with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
161 log_path = self._event_log_module.Write(path=tempdir)
162 self._log_data = self.readLog(log_path)
163
164 self.assertEqual(len(self._log_data), 2)
165 exit_event = self._log_data[1]
166 self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
167 self.verifyCommonKeys(exit_event, expected_event_name='exit')
168 # Check for 'exit' event specific fields.
169 self.assertIn('code', exit_event)
170 self.assertEqual(exit_event['code'], 2)
171
172 def test_command_event(self):
173 """Test and validate 'command' event data is valid.
174
175 Expected event log:
176 <version event>
177 <command event>
178 """
179 name = 'repo'
180 subcommands = ['init' 'this']
181 self._event_log_module.CommandEvent(name='repo', subcommands=subcommands)
182 with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
183 log_path = self._event_log_module.Write(path=tempdir)
184 self._log_data = self.readLog(log_path)
185
186 self.assertEqual(len(self._log_data), 2)
187 command_event = self._log_data[1]
188 self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
189 self.verifyCommonKeys(command_event, expected_event_name='command')
190 # Check for 'command' event specific fields.
191 self.assertIn('name', command_event)
192 self.assertIn('subcommands', command_event)
193 self.assertEqual(command_event['name'], name)
194 self.assertEqual(command_event['subcommands'], subcommands)
195
196 def test_def_params_event_repo_config(self):
197 """Test 'def_params' event data outputs only repo config keys.
198
199 Expected event log:
200 <version event>
201 <def_param event>
202 <def_param event>
203 """
204 config = {
205 'git.foo': 'bar',
206 'repo.partialclone': 'true',
207 'repo.partialclonefilter': 'blob:none',
208 }
209 self._event_log_module.DefParamRepoEvents(config)
210
211 with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
212 log_path = self._event_log_module.Write(path=tempdir)
213 self._log_data = self.readLog(log_path)
214
215 self.assertEqual(len(self._log_data), 3)
216 def_param_events = self._log_data[1:]
217 self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
218
219 for event in def_param_events:
220 self.verifyCommonKeys(event, expected_event_name='def_param')
221 # Check for 'def_param' event specific fields.
222 self.assertIn('param', event)
223 self.assertIn('value', event)
224 self.assertTrue(event['param'].startswith('repo.'))
225
226 def test_def_params_event_no_repo_config(self):
227 """Test 'def_params' event data won't output non-repo config keys.
228
229 Expected event log:
230 <version event>
231 """
232 config = {
233 'git.foo': 'bar',
234 'git.core.foo2': 'baz',
235 }
236 self._event_log_module.DefParamRepoEvents(config)
237
238 with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
239 log_path = self._event_log_module.Write(path=tempdir)
240 self._log_data = self.readLog(log_path)
241
242 self.assertEqual(len(self._log_data), 1)
243 self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
244
245 def test_data_event_config(self):
246 """Test 'data' event data outputs all config keys.
247
248 Expected event log:
249 <version event>
250 <data event>
251 <data event>
252 """
253 config = {
254 'git.foo': 'bar',
255 'repo.partialclone': 'false',
256 'repo.syncstate.superproject.hassuperprojecttag': 'true',
257 'repo.syncstate.superproject.sys.argv': ['--', 'sync', 'protobuf'],
258 }
259 prefix_value = 'prefix'
260 self._event_log_module.LogDataConfigEvents(config, prefix_value)
261
262 with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
263 log_path = self._event_log_module.Write(path=tempdir)
264 self._log_data = self.readLog(log_path)
265
266 self.assertEqual(len(self._log_data), 5)
267 data_events = self._log_data[1:]
268 self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
269
270 for event in data_events:
271 self.verifyCommonKeys(event)
272 # Check for 'data' event specific fields.
273 self.assertIn('key', event)
274 self.assertIn('value', event)
275 key = event['key']
276 key = self.remove_prefix(key, f'{prefix_value}/')
277 value = event['value']
278 self.assertEqual(self._event_log_module.GetDataEventName(value), event['event'])
279 self.assertTrue(key in config and value == config[key])
280
281 def test_error_event(self):
282 """Test and validate 'error' event data is valid.
283
284 Expected event log:
285 <version event>
286 <error event>
287 """
288 msg = 'invalid option: --cahced'
289 fmt = 'invalid option: %s'
290 self._event_log_module.ErrorEvent(msg, fmt)
291 with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
292 log_path = self._event_log_module.Write(path=tempdir)
293 self._log_data = self.readLog(log_path)
294
295 self.assertEqual(len(self._log_data), 2)
296 error_event = self._log_data[1]
297 self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
298 self.verifyCommonKeys(error_event, expected_event_name='error')
299 # Check for 'error' event specific fields.
300 self.assertIn('msg', error_event)
301 self.assertIn('fmt', error_event)
302 self.assertEqual(error_event['msg'], msg)
303 self.assertEqual(error_event['fmt'], fmt)
304
305 def test_write_with_filename(self):
306 """Test Write() with a path to a file exits with None."""
307 self.assertIsNone(self._event_log_module.Write(path='path/to/file'))
308
309 def test_write_with_git_config(self):
310 """Test Write() uses the git config path when 'git config' call succeeds."""
311 with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
312 with mock.patch.object(self._event_log_module,
313 '_GetEventTargetPath', return_value=tempdir):
314 self.assertEqual(os.path.dirname(self._event_log_module.Write()), tempdir)
315
316 def test_write_no_git_config(self):
317 """Test Write() with no git config variable present exits with None."""
318 with mock.patch.object(self._event_log_module,
319 '_GetEventTargetPath', return_value=None):
320 self.assertIsNone(self._event_log_module.Write())
321
322 def test_write_non_string(self):
323 """Test Write() with non-string type for |path| throws TypeError."""
324 with self.assertRaises(TypeError):
325 self._event_log_module.Write(path=1234)
326
327
328if __name__ == '__main__':
329 unittest.main()
diff --git a/tests/test_hooks.py b/tests/test_hooks.py
new file mode 100644
index 00000000..6632b3e5
--- /dev/null
+++ b/tests/test_hooks.py
@@ -0,0 +1,55 @@
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
17import hooks
18import unittest
19
20class RepoHookShebang(unittest.TestCase):
21 """Check shebang parsing in RepoHook."""
22
23 def test_no_shebang(self):
24 """Lines w/out shebangs should be rejected."""
25 DATA = (
26 '',
27 '#\n# foo\n',
28 '# Bad shebang in script\n#!/foo\n'
29 )
30 for data in DATA:
31 self.assertIsNone(hooks.RepoHook._ExtractInterpFromShebang(data))
32
33 def test_direct_interp(self):
34 """Lines whose shebang points directly to the interpreter."""
35 DATA = (
36 ('#!/foo', '/foo'),
37 ('#! /foo', '/foo'),
38 ('#!/bin/foo ', '/bin/foo'),
39 ('#! /usr/foo ', '/usr/foo'),
40 ('#! /usr/foo -args', '/usr/foo'),
41 )
42 for shebang, interp in DATA:
43 self.assertEqual(hooks.RepoHook._ExtractInterpFromShebang(shebang),
44 interp)
45
46 def test_env_interp(self):
47 """Lines whose shebang launches through `env`."""
48 DATA = (
49 ('#!/usr/bin/env foo', 'foo'),
50 ('#!/bin/env foo', 'foo'),
51 ('#! /bin/env /bin/foo ', '/bin/foo'),
52 )
53 for shebang, interp in DATA:
54 self.assertEqual(hooks.RepoHook._ExtractInterpFromShebang(shebang),
55 interp)
diff --git a/tests/test_manifest_xml.py b/tests/test_manifest_xml.py
new file mode 100644
index 00000000..cb3eb855
--- /dev/null
+++ b/tests/test_manifest_xml.py
@@ -0,0 +1,845 @@
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 manifest_xml.py module."""
16
17import os
18import platform
19import re
20import shutil
21import tempfile
22import unittest
23import xml.dom.minidom
24
25import error
26import manifest_xml
27
28
29# Invalid paths that we don't want in the filesystem.
30INVALID_FS_PATHS = (
31 '',
32 '.',
33 '..',
34 '../',
35 './',
36 './/',
37 'foo/',
38 './foo',
39 '../foo',
40 'foo/./bar',
41 'foo/../../bar',
42 '/foo',
43 './../foo',
44 '.git/foo',
45 # Check case folding.
46 '.GIT/foo',
47 'blah/.git/foo',
48 '.repo/foo',
49 '.repoconfig',
50 # Block ~ due to 8.3 filenames on Windows filesystems.
51 '~',
52 'foo~',
53 'blah/foo~',
54 # Block Unicode characters that get normalized out by filesystems.
55 u'foo\u200Cbar',
56 # Block newlines.
57 'f\n/bar',
58 'f\r/bar',
59)
60
61# Make sure platforms that use path separators (e.g. Windows) are also
62# rejected properly.
63if os.path.sep != '/':
64 INVALID_FS_PATHS += tuple(x.replace('/', os.path.sep) for x in INVALID_FS_PATHS)
65
66
67def sort_attributes(manifest):
68 """Sort the attributes of all elements alphabetically.
69
70 This is needed because different versions of the toxml() function from
71 xml.dom.minidom outputs the attributes of elements in different orders.
72 Before Python 3.8 they were output alphabetically, later versions preserve
73 the order specified by the user.
74
75 Args:
76 manifest: String containing an XML manifest.
77
78 Returns:
79 The XML manifest with the attributes of all elements sorted alphabetically.
80 """
81 new_manifest = ''
82 # This will find every element in the XML manifest, whether they have
83 # attributes or not. This simplifies recreating the manifest below.
84 matches = re.findall(r'(<[/?]?[a-z-]+\s*)((?:\S+?="[^"]+"\s*?)*)(\s*[/?]?>)', manifest)
85 for head, attrs, tail in matches:
86 m = re.findall(r'\S+?="[^"]+"', attrs)
87 new_manifest += head + ' '.join(sorted(m)) + tail
88 return new_manifest
89
90
91class ManifestParseTestCase(unittest.TestCase):
92 """TestCase for parsing manifests."""
93
94 def setUp(self):
95 self.tempdir = tempfile.mkdtemp(prefix='repo_tests')
96 self.repodir = os.path.join(self.tempdir, '.repo')
97 self.manifest_dir = os.path.join(self.repodir, 'manifests')
98 self.manifest_file = os.path.join(
99 self.repodir, manifest_xml.MANIFEST_FILE_NAME)
100 self.local_manifest_dir = os.path.join(
101 self.repodir, manifest_xml.LOCAL_MANIFESTS_DIR_NAME)
102 os.mkdir(self.repodir)
103 os.mkdir(self.manifest_dir)
104
105 # The manifest parsing really wants a git repo currently.
106 gitdir = os.path.join(self.repodir, 'manifests.git')
107 os.mkdir(gitdir)
108 with open(os.path.join(gitdir, 'config'), 'w') as fp:
109 fp.write("""[remote "origin"]
110 url = https://localhost:0/manifest
111""")
112
113 def tearDown(self):
114 shutil.rmtree(self.tempdir, ignore_errors=True)
115
116 def getXmlManifest(self, data):
117 """Helper to initialize a manifest for testing."""
118 with open(self.manifest_file, 'w') as fp:
119 fp.write(data)
120 return manifest_xml.XmlManifest(self.repodir, self.manifest_file)
121
122 @staticmethod
123 def encodeXmlAttr(attr):
124 """Encode |attr| using XML escape rules."""
125 return attr.replace('\r', '&#x000d;').replace('\n', '&#x000a;')
126
127
128class ManifestValidateFilePaths(unittest.TestCase):
129 """Check _ValidateFilePaths helper.
130
131 This doesn't access a real filesystem.
132 """
133
134 def check_both(self, *args):
135 manifest_xml.XmlManifest._ValidateFilePaths('copyfile', *args)
136 manifest_xml.XmlManifest._ValidateFilePaths('linkfile', *args)
137
138 def test_normal_path(self):
139 """Make sure good paths are accepted."""
140 self.check_both('foo', 'bar')
141 self.check_both('foo/bar', 'bar')
142 self.check_both('foo', 'bar/bar')
143 self.check_both('foo/bar', 'bar/bar')
144
145 def test_symlink_targets(self):
146 """Some extra checks for symlinks."""
147 def check(*args):
148 manifest_xml.XmlManifest._ValidateFilePaths('linkfile', *args)
149
150 # We allow symlinks to end in a slash since we allow them to point to dirs
151 # in general. Technically the slash isn't necessary.
152 check('foo/', 'bar')
153 # We allow a single '.' to get a reference to the project itself.
154 check('.', 'bar')
155
156 def test_bad_paths(self):
157 """Make sure bad paths (src & dest) are rejected."""
158 for path in INVALID_FS_PATHS:
159 self.assertRaises(
160 error.ManifestInvalidPathError, self.check_both, path, 'a')
161 self.assertRaises(
162 error.ManifestInvalidPathError, self.check_both, 'a', path)
163
164
165class ValueTests(unittest.TestCase):
166 """Check utility parsing code."""
167
168 def _get_node(self, text):
169 return xml.dom.minidom.parseString(text).firstChild
170
171 def test_bool_default(self):
172 """Check XmlBool default handling."""
173 node = self._get_node('<node/>')
174 self.assertIsNone(manifest_xml.XmlBool(node, 'a'))
175 self.assertIsNone(manifest_xml.XmlBool(node, 'a', None))
176 self.assertEqual(123, manifest_xml.XmlBool(node, 'a', 123))
177
178 node = self._get_node('<node a=""/>')
179 self.assertIsNone(manifest_xml.XmlBool(node, 'a'))
180
181 def test_bool_invalid(self):
182 """Check XmlBool invalid handling."""
183 node = self._get_node('<node a="moo"/>')
184 self.assertEqual(123, manifest_xml.XmlBool(node, 'a', 123))
185
186 def test_bool_true(self):
187 """Check XmlBool true values."""
188 for value in ('yes', 'true', '1'):
189 node = self._get_node('<node a="%s"/>' % (value,))
190 self.assertTrue(manifest_xml.XmlBool(node, 'a'))
191
192 def test_bool_false(self):
193 """Check XmlBool false values."""
194 for value in ('no', 'false', '0'):
195 node = self._get_node('<node a="%s"/>' % (value,))
196 self.assertFalse(manifest_xml.XmlBool(node, 'a'))
197
198 def test_int_default(self):
199 """Check XmlInt default handling."""
200 node = self._get_node('<node/>')
201 self.assertIsNone(manifest_xml.XmlInt(node, 'a'))
202 self.assertIsNone(manifest_xml.XmlInt(node, 'a', None))
203 self.assertEqual(123, manifest_xml.XmlInt(node, 'a', 123))
204
205 node = self._get_node('<node a=""/>')
206 self.assertIsNone(manifest_xml.XmlInt(node, 'a'))
207
208 def test_int_good(self):
209 """Check XmlInt numeric handling."""
210 for value in (-1, 0, 1, 50000):
211 node = self._get_node('<node a="%s"/>' % (value,))
212 self.assertEqual(value, manifest_xml.XmlInt(node, 'a'))
213
214 def test_int_invalid(self):
215 """Check XmlInt invalid handling."""
216 with self.assertRaises(error.ManifestParseError):
217 node = self._get_node('<node a="xx"/>')
218 manifest_xml.XmlInt(node, 'a')
219
220
221class XmlManifestTests(ManifestParseTestCase):
222 """Check manifest processing."""
223
224 def test_empty(self):
225 """Parse an 'empty' manifest file."""
226 manifest = self.getXmlManifest(
227 '<?xml version="1.0" encoding="UTF-8"?>'
228 '<manifest></manifest>')
229 self.assertEqual(manifest.remotes, {})
230 self.assertEqual(manifest.projects, [])
231
232 def test_link(self):
233 """Verify Link handling with new names."""
234 manifest = manifest_xml.XmlManifest(self.repodir, self.manifest_file)
235 with open(os.path.join(self.manifest_dir, 'foo.xml'), 'w') as fp:
236 fp.write('<manifest></manifest>')
237 manifest.Link('foo.xml')
238 with open(self.manifest_file) as fp:
239 self.assertIn('<include name="foo.xml" />', fp.read())
240
241 def test_toxml_empty(self):
242 """Verify the ToXml() helper."""
243 manifest = self.getXmlManifest(
244 '<?xml version="1.0" encoding="UTF-8"?>'
245 '<manifest></manifest>')
246 self.assertEqual(manifest.ToXml().toxml(), '<?xml version="1.0" ?><manifest/>')
247
248 def test_todict_empty(self):
249 """Verify the ToDict() helper."""
250 manifest = self.getXmlManifest(
251 '<?xml version="1.0" encoding="UTF-8"?>'
252 '<manifest></manifest>')
253 self.assertEqual(manifest.ToDict(), {})
254
255 def test_repo_hooks(self):
256 """Check repo-hooks settings."""
257 manifest = self.getXmlManifest("""
258<manifest>
259 <remote name="test-remote" fetch="http://localhost" />
260 <default remote="test-remote" revision="refs/heads/main" />
261 <project name="repohooks" path="src/repohooks"/>
262 <repo-hooks in-project="repohooks" enabled-list="a, b"/>
263</manifest>
264""")
265 self.assertEqual(manifest.repo_hooks_project.name, 'repohooks')
266 self.assertEqual(manifest.repo_hooks_project.enabled_repo_hooks, ['a', 'b'])
267
268 def test_repo_hooks_unordered(self):
269 """Check repo-hooks settings work even if the project def comes second."""
270 manifest = self.getXmlManifest("""
271<manifest>
272 <remote name="test-remote" fetch="http://localhost" />
273 <default remote="test-remote" revision="refs/heads/main" />
274 <repo-hooks in-project="repohooks" enabled-list="a, b"/>
275 <project name="repohooks" path="src/repohooks"/>
276</manifest>
277""")
278 self.assertEqual(manifest.repo_hooks_project.name, 'repohooks')
279 self.assertEqual(manifest.repo_hooks_project.enabled_repo_hooks, ['a', 'b'])
280
281 def test_unknown_tags(self):
282 """Check superproject settings."""
283 manifest = self.getXmlManifest("""
284<manifest>
285 <remote name="test-remote" fetch="http://localhost" />
286 <default remote="test-remote" revision="refs/heads/main" />
287 <superproject name="superproject"/>
288 <iankaz value="unknown (possible) future tags are ignored"/>
289 <x-custom-tag>X tags are always ignored</x-custom-tag>
290</manifest>
291""")
292 self.assertEqual(manifest.superproject['name'], 'superproject')
293 self.assertEqual(manifest.superproject['remote'].name, 'test-remote')
294 self.assertEqual(
295 sort_attributes(manifest.ToXml().toxml()),
296 '<?xml version="1.0" ?><manifest>'
297 '<remote fetch="http://localhost" name="test-remote"/>'
298 '<default remote="test-remote" revision="refs/heads/main"/>'
299 '<superproject name="superproject"/>'
300 '</manifest>')
301
302 def test_remote_annotations(self):
303 """Check remote settings."""
304 manifest = self.getXmlManifest("""
305<manifest>
306 <remote name="test-remote" fetch="http://localhost">
307 <annotation name="foo" value="bar"/>
308 </remote>
309</manifest>
310""")
311 self.assertEqual(manifest.remotes['test-remote'].annotations[0].name, 'foo')
312 self.assertEqual(manifest.remotes['test-remote'].annotations[0].value, 'bar')
313 self.assertEqual(
314 sort_attributes(manifest.ToXml().toxml()),
315 '<?xml version="1.0" ?><manifest>'
316 '<remote fetch="http://localhost" name="test-remote">'
317 '<annotation name="foo" value="bar"/>'
318 '</remote>'
319 '</manifest>')
320
321
322class IncludeElementTests(ManifestParseTestCase):
323 """Tests for <include>."""
324
325 def test_group_levels(self):
326 root_m = os.path.join(self.manifest_dir, 'root.xml')
327 with open(root_m, 'w') as fp:
328 fp.write("""
329<manifest>
330 <remote name="test-remote" fetch="http://localhost" />
331 <default remote="test-remote" revision="refs/heads/main" />
332 <include name="level1.xml" groups="level1-group" />
333 <project name="root-name1" path="root-path1" />
334 <project name="root-name2" path="root-path2" groups="r2g1,r2g2" />
335</manifest>
336""")
337 with open(os.path.join(self.manifest_dir, 'level1.xml'), 'w') as fp:
338 fp.write("""
339<manifest>
340 <include name="level2.xml" groups="level2-group" />
341 <project name="level1-name1" path="level1-path1" />
342</manifest>
343""")
344 with open(os.path.join(self.manifest_dir, 'level2.xml'), 'w') as fp:
345 fp.write("""
346<manifest>
347 <project name="level2-name1" path="level2-path1" groups="l2g1,l2g2" />
348</manifest>
349""")
350 include_m = manifest_xml.XmlManifest(self.repodir, root_m)
351 for proj in include_m.projects:
352 if proj.name == 'root-name1':
353 # Check include group not set on root level proj.
354 self.assertNotIn('level1-group', proj.groups)
355 if proj.name == 'root-name2':
356 # Check root proj group not removed.
357 self.assertIn('r2g1', proj.groups)
358 if proj.name == 'level1-name1':
359 # Check level1 proj has inherited group level 1.
360 self.assertIn('level1-group', proj.groups)
361 if proj.name == 'level2-name1':
362 # Check level2 proj has inherited group levels 1 and 2.
363 self.assertIn('level1-group', proj.groups)
364 self.assertIn('level2-group', proj.groups)
365 # Check level2 proj group not removed.
366 self.assertIn('l2g1', proj.groups)
367
368 def test_allow_bad_name_from_user(self):
369 """Check handling of bad name attribute from the user's input."""
370 def parse(name):
371 name = self.encodeXmlAttr(name)
372 manifest = self.getXmlManifest(f"""
373<manifest>
374 <remote name="default-remote" fetch="http://localhost" />
375 <default remote="default-remote" revision="refs/heads/main" />
376 <include name="{name}" />
377</manifest>
378""")
379 # Force the manifest to be parsed.
380 manifest.ToXml()
381
382 # Setup target of the include.
383 target = os.path.join(self.tempdir, 'target.xml')
384 with open(target, 'w') as fp:
385 fp.write('<manifest></manifest>')
386
387 # Include with absolute path.
388 parse(os.path.abspath(target))
389
390 # Include with relative path.
391 parse(os.path.relpath(target, self.manifest_dir))
392
393 def test_bad_name_checks(self):
394 """Check handling of bad name attribute."""
395 def parse(name):
396 name = self.encodeXmlAttr(name)
397 # Setup target of the include.
398 with open(os.path.join(self.manifest_dir, 'target.xml'), 'w') as fp:
399 fp.write(f'<manifest><include name="{name}"/></manifest>')
400
401 manifest = self.getXmlManifest("""
402<manifest>
403 <remote name="default-remote" fetch="http://localhost" />
404 <default remote="default-remote" revision="refs/heads/main" />
405 <include name="target.xml" />
406</manifest>
407""")
408 # Force the manifest to be parsed.
409 manifest.ToXml()
410
411 # Handle empty name explicitly because a different codepath rejects it.
412 with self.assertRaises(error.ManifestParseError):
413 parse('')
414
415 for path in INVALID_FS_PATHS:
416 if not path:
417 continue
418
419 with self.assertRaises(error.ManifestInvalidPathError):
420 parse(path)
421
422
423class ProjectElementTests(ManifestParseTestCase):
424 """Tests for <project>."""
425
426 def test_group(self):
427 """Check project group settings."""
428 manifest = self.getXmlManifest("""
429<manifest>
430 <remote name="test-remote" fetch="http://localhost" />
431 <default remote="test-remote" revision="refs/heads/main" />
432 <project name="test-name" path="test-path"/>
433 <project name="extras" path="path" groups="g1,g2,g1"/>
434</manifest>
435""")
436 self.assertEqual(len(manifest.projects), 2)
437 # Ordering isn't guaranteed.
438 result = {
439 manifest.projects[0].name: manifest.projects[0].groups,
440 manifest.projects[1].name: manifest.projects[1].groups,
441 }
442 project = manifest.projects[0]
443 self.assertCountEqual(
444 result['test-name'],
445 ['name:test-name', 'all', 'path:test-path'])
446 self.assertCountEqual(
447 result['extras'],
448 ['g1', 'g2', 'g1', 'name:extras', 'all', 'path:path'])
449 groupstr = 'default,platform-' + platform.system().lower()
450 self.assertEqual(groupstr, manifest.GetGroupsStr())
451 groupstr = 'g1,g2,g1'
452 manifest.manifestProject.config.SetString('manifest.groups', groupstr)
453 self.assertEqual(groupstr, manifest.GetGroupsStr())
454
455 def test_set_revision_id(self):
456 """Check setting of project's revisionId."""
457 manifest = self.getXmlManifest("""
458<manifest>
459 <remote name="default-remote" fetch="http://localhost" />
460 <default remote="default-remote" revision="refs/heads/main" />
461 <project name="test-name"/>
462</manifest>
463""")
464 self.assertEqual(len(manifest.projects), 1)
465 project = manifest.projects[0]
466 project.SetRevisionId('ABCDEF')
467 self.assertEqual(
468 sort_attributes(manifest.ToXml().toxml()),
469 '<?xml version="1.0" ?><manifest>'
470 '<remote fetch="http://localhost" name="default-remote"/>'
471 '<default remote="default-remote" revision="refs/heads/main"/>'
472 '<project name="test-name" revision="ABCDEF" upstream="refs/heads/main"/>'
473 '</manifest>')
474
475 def test_trailing_slash(self):
476 """Check handling of trailing slashes in attributes."""
477 def parse(name, path):
478 name = self.encodeXmlAttr(name)
479 path = self.encodeXmlAttr(path)
480 return self.getXmlManifest(f"""
481<manifest>
482 <remote name="default-remote" fetch="http://localhost" />
483 <default remote="default-remote" revision="refs/heads/main" />
484 <project name="{name}" path="{path}" />
485</manifest>
486""")
487
488 manifest = parse('a/path/', 'foo')
489 self.assertEqual(manifest.projects[0].gitdir,
490 os.path.join(self.tempdir, '.repo/projects/foo.git'))
491 self.assertEqual(manifest.projects[0].objdir,
492 os.path.join(self.tempdir, '.repo/project-objects/a/path.git'))
493
494 manifest = parse('a/path', 'foo/')
495 self.assertEqual(manifest.projects[0].gitdir,
496 os.path.join(self.tempdir, '.repo/projects/foo.git'))
497 self.assertEqual(manifest.projects[0].objdir,
498 os.path.join(self.tempdir, '.repo/project-objects/a/path.git'))
499
500 manifest = parse('a/path', 'foo//////')
501 self.assertEqual(manifest.projects[0].gitdir,
502 os.path.join(self.tempdir, '.repo/projects/foo.git'))
503 self.assertEqual(manifest.projects[0].objdir,
504 os.path.join(self.tempdir, '.repo/project-objects/a/path.git'))
505
506 def test_toplevel_path(self):
507 """Check handling of path=. specially."""
508 def parse(name, path):
509 name = self.encodeXmlAttr(name)
510 path = self.encodeXmlAttr(path)
511 return self.getXmlManifest(f"""
512<manifest>
513 <remote name="default-remote" fetch="http://localhost" />
514 <default remote="default-remote" revision="refs/heads/main" />
515 <project name="{name}" path="{path}" />
516</manifest>
517""")
518
519 for path in ('.', './', './/', './//'):
520 manifest = parse('server/path', path)
521 self.assertEqual(manifest.projects[0].gitdir,
522 os.path.join(self.tempdir, '.repo/projects/..git'))
523
524 def test_bad_path_name_checks(self):
525 """Check handling of bad path & name attributes."""
526 def parse(name, path):
527 name = self.encodeXmlAttr(name)
528 path = self.encodeXmlAttr(path)
529 manifest = self.getXmlManifest(f"""
530<manifest>
531 <remote name="default-remote" fetch="http://localhost" />
532 <default remote="default-remote" revision="refs/heads/main" />
533 <project name="{name}" path="{path}" />
534</manifest>
535""")
536 # Force the manifest to be parsed.
537 manifest.ToXml()
538
539 # Verify the parser is valid by default to avoid buggy tests below.
540 parse('ok', 'ok')
541
542 # Handle empty name explicitly because a different codepath rejects it.
543 # Empty path is OK because it defaults to the name field.
544 with self.assertRaises(error.ManifestParseError):
545 parse('', 'ok')
546
547 for path in INVALID_FS_PATHS:
548 if not path or path.endswith('/'):
549 continue
550
551 with self.assertRaises(error.ManifestInvalidPathError):
552 parse(path, 'ok')
553
554 # We have a dedicated test for path=".".
555 if path not in {'.'}:
556 with self.assertRaises(error.ManifestInvalidPathError):
557 parse('ok', path)
558
559
560class SuperProjectElementTests(ManifestParseTestCase):
561 """Tests for <superproject>."""
562
563 def test_superproject(self):
564 """Check superproject settings."""
565 manifest = self.getXmlManifest("""
566<manifest>
567 <remote name="test-remote" fetch="http://localhost" />
568 <default remote="test-remote" revision="refs/heads/main" />
569 <superproject name="superproject"/>
570</manifest>
571""")
572 self.assertEqual(manifest.superproject['name'], 'superproject')
573 self.assertEqual(manifest.superproject['remote'].name, 'test-remote')
574 self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/superproject')
575 self.assertEqual(manifest.superproject['revision'], 'refs/heads/main')
576 self.assertEqual(
577 sort_attributes(manifest.ToXml().toxml()),
578 '<?xml version="1.0" ?><manifest>'
579 '<remote fetch="http://localhost" name="test-remote"/>'
580 '<default remote="test-remote" revision="refs/heads/main"/>'
581 '<superproject name="superproject"/>'
582 '</manifest>')
583
584 def test_superproject_revision(self):
585 """Check superproject settings with a different revision attribute"""
586 self.maxDiff = None
587 manifest = self.getXmlManifest("""
588<manifest>
589 <remote name="test-remote" fetch="http://localhost" />
590 <default remote="test-remote" revision="refs/heads/main" />
591 <superproject name="superproject" revision="refs/heads/stable" />
592</manifest>
593""")
594 self.assertEqual(manifest.superproject['name'], 'superproject')
595 self.assertEqual(manifest.superproject['remote'].name, 'test-remote')
596 self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/superproject')
597 self.assertEqual(manifest.superproject['revision'], 'refs/heads/stable')
598 self.assertEqual(
599 sort_attributes(manifest.ToXml().toxml()),
600 '<?xml version="1.0" ?><manifest>'
601 '<remote fetch="http://localhost" name="test-remote"/>'
602 '<default remote="test-remote" revision="refs/heads/main"/>'
603 '<superproject name="superproject" revision="refs/heads/stable"/>'
604 '</manifest>')
605
606 def test_superproject_revision_default_negative(self):
607 """Check superproject settings with a same revision attribute"""
608 self.maxDiff = None
609 manifest = self.getXmlManifest("""
610<manifest>
611 <remote name="test-remote" fetch="http://localhost" />
612 <default remote="test-remote" revision="refs/heads/stable" />
613 <superproject name="superproject" revision="refs/heads/stable" />
614</manifest>
615""")
616 self.assertEqual(manifest.superproject['name'], 'superproject')
617 self.assertEqual(manifest.superproject['remote'].name, 'test-remote')
618 self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/superproject')
619 self.assertEqual(manifest.superproject['revision'], 'refs/heads/stable')
620 self.assertEqual(
621 sort_attributes(manifest.ToXml().toxml()),
622 '<?xml version="1.0" ?><manifest>'
623 '<remote fetch="http://localhost" name="test-remote"/>'
624 '<default remote="test-remote" revision="refs/heads/stable"/>'
625 '<superproject name="superproject"/>'
626 '</manifest>')
627
628 def test_superproject_revision_remote(self):
629 """Check superproject settings with a same revision attribute"""
630 self.maxDiff = None
631 manifest = self.getXmlManifest("""
632<manifest>
633 <remote name="test-remote" fetch="http://localhost" revision="refs/heads/main" />
634 <default remote="test-remote" />
635 <superproject name="superproject" revision="refs/heads/stable" />
636</manifest>
637""")
638 self.assertEqual(manifest.superproject['name'], 'superproject')
639 self.assertEqual(manifest.superproject['remote'].name, 'test-remote')
640 self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/superproject')
641 self.assertEqual(manifest.superproject['revision'], 'refs/heads/stable')
642 self.assertEqual(
643 sort_attributes(manifest.ToXml().toxml()),
644 '<?xml version="1.0" ?><manifest>'
645 '<remote fetch="http://localhost" name="test-remote" revision="refs/heads/main"/>'
646 '<default remote="test-remote"/>'
647 '<superproject name="superproject" revision="refs/heads/stable"/>'
648 '</manifest>')
649
650 def test_remote(self):
651 """Check superproject settings with a remote."""
652 manifest = self.getXmlManifest("""
653<manifest>
654 <remote name="default-remote" fetch="http://localhost" />
655 <remote name="superproject-remote" fetch="http://localhost" />
656 <default remote="default-remote" revision="refs/heads/main" />
657 <superproject name="platform/superproject" remote="superproject-remote"/>
658</manifest>
659""")
660 self.assertEqual(manifest.superproject['name'], 'platform/superproject')
661 self.assertEqual(manifest.superproject['remote'].name, 'superproject-remote')
662 self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/platform/superproject')
663 self.assertEqual(manifest.superproject['revision'], 'refs/heads/main')
664 self.assertEqual(
665 sort_attributes(manifest.ToXml().toxml()),
666 '<?xml version="1.0" ?><manifest>'
667 '<remote fetch="http://localhost" name="default-remote"/>'
668 '<remote fetch="http://localhost" name="superproject-remote"/>'
669 '<default remote="default-remote" revision="refs/heads/main"/>'
670 '<superproject name="platform/superproject" remote="superproject-remote"/>'
671 '</manifest>')
672
673 def test_defalut_remote(self):
674 """Check superproject settings with a default remote."""
675 manifest = self.getXmlManifest("""
676<manifest>
677 <remote name="default-remote" fetch="http://localhost" />
678 <default remote="default-remote" revision="refs/heads/main" />
679 <superproject name="superproject" remote="default-remote"/>
680</manifest>
681""")
682 self.assertEqual(manifest.superproject['name'], 'superproject')
683 self.assertEqual(manifest.superproject['remote'].name, 'default-remote')
684 self.assertEqual(manifest.superproject['revision'], 'refs/heads/main')
685 self.assertEqual(
686 sort_attributes(manifest.ToXml().toxml()),
687 '<?xml version="1.0" ?><manifest>'
688 '<remote fetch="http://localhost" name="default-remote"/>'
689 '<default remote="default-remote" revision="refs/heads/main"/>'
690 '<superproject name="superproject"/>'
691 '</manifest>')
692
693
694class ContactinfoElementTests(ManifestParseTestCase):
695 """Tests for <contactinfo>."""
696
697 def test_contactinfo(self):
698 """Check contactinfo settings."""
699 bugurl = 'http://localhost/contactinfo'
700 manifest = self.getXmlManifest(f"""
701<manifest>
702 <contactinfo bugurl="{bugurl}"/>
703</manifest>
704""")
705 self.assertEqual(manifest.contactinfo.bugurl, bugurl)
706 self.assertEqual(
707 manifest.ToXml().toxml(),
708 '<?xml version="1.0" ?><manifest>'
709 f'<contactinfo bugurl="{bugurl}"/>'
710 '</manifest>')
711
712
713class DefaultElementTests(ManifestParseTestCase):
714 """Tests for <default>."""
715
716 def test_default(self):
717 """Check default settings."""
718 a = manifest_xml._Default()
719 a.revisionExpr = 'foo'
720 a.remote = manifest_xml._XmlRemote(name='remote')
721 b = manifest_xml._Default()
722 b.revisionExpr = 'bar'
723 self.assertEqual(a, a)
724 self.assertNotEqual(a, b)
725 self.assertNotEqual(b, a.remote)
726 self.assertNotEqual(a, 123)
727 self.assertNotEqual(a, None)
728
729
730class RemoteElementTests(ManifestParseTestCase):
731 """Tests for <remote>."""
732
733 def test_remote(self):
734 """Check remote settings."""
735 a = manifest_xml._XmlRemote(name='foo')
736 a.AddAnnotation('key1', 'value1', 'true')
737 b = manifest_xml._XmlRemote(name='foo')
738 b.AddAnnotation('key2', 'value1', 'true')
739 c = manifest_xml._XmlRemote(name='foo')
740 c.AddAnnotation('key1', 'value2', 'true')
741 d = manifest_xml._XmlRemote(name='foo')
742 d.AddAnnotation('key1', 'value1', 'false')
743 self.assertEqual(a, a)
744 self.assertNotEqual(a, b)
745 self.assertNotEqual(a, c)
746 self.assertNotEqual(a, d)
747 self.assertNotEqual(a, manifest_xml._Default())
748 self.assertNotEqual(a, 123)
749 self.assertNotEqual(a, None)
750
751
752class RemoveProjectElementTests(ManifestParseTestCase):
753 """Tests for <remove-project>."""
754
755 def test_remove_one_project(self):
756 manifest = self.getXmlManifest("""
757<manifest>
758 <remote name="default-remote" fetch="http://localhost" />
759 <default remote="default-remote" revision="refs/heads/main" />
760 <project name="myproject" />
761 <remove-project name="myproject" />
762</manifest>
763""")
764 self.assertEqual(manifest.projects, [])
765
766 def test_remove_one_project_one_remains(self):
767 manifest = self.getXmlManifest("""
768<manifest>
769 <remote name="default-remote" fetch="http://localhost" />
770 <default remote="default-remote" revision="refs/heads/main" />
771 <project name="myproject" />
772 <project name="yourproject" />
773 <remove-project name="myproject" />
774</manifest>
775""")
776
777 self.assertEqual(len(manifest.projects), 1)
778 self.assertEqual(manifest.projects[0].name, 'yourproject')
779
780 def test_remove_one_project_doesnt_exist(self):
781 with self.assertRaises(manifest_xml.ManifestParseError):
782 manifest = self.getXmlManifest("""
783<manifest>
784 <remote name="default-remote" fetch="http://localhost" />
785 <default remote="default-remote" revision="refs/heads/main" />
786 <remove-project name="myproject" />
787</manifest>
788""")
789 manifest.projects
790
791 def test_remove_one_optional_project_doesnt_exist(self):
792 manifest = self.getXmlManifest("""
793<manifest>
794 <remote name="default-remote" fetch="http://localhost" />
795 <default remote="default-remote" revision="refs/heads/main" />
796 <remove-project name="myproject" optional="true" />
797</manifest>
798""")
799 self.assertEqual(manifest.projects, [])
800
801
802class ExtendProjectElementTests(ManifestParseTestCase):
803 """Tests for <extend-project>."""
804
805 def test_extend_project_dest_path_single_match(self):
806 manifest = self.getXmlManifest("""
807<manifest>
808 <remote name="default-remote" fetch="http://localhost" />
809 <default remote="default-remote" revision="refs/heads/main" />
810 <project name="myproject" />
811 <extend-project name="myproject" dest-path="bar" />
812</manifest>
813""")
814 self.assertEqual(len(manifest.projects), 1)
815 self.assertEqual(manifest.projects[0].relpath, 'bar')
816
817 def test_extend_project_dest_path_multi_match(self):
818 with self.assertRaises(manifest_xml.ManifestParseError):
819 manifest = self.getXmlManifest("""
820<manifest>
821 <remote name="default-remote" fetch="http://localhost" />
822 <default remote="default-remote" revision="refs/heads/main" />
823 <project name="myproject" path="x" />
824 <project name="myproject" path="y" />
825 <extend-project name="myproject" dest-path="bar" />
826</manifest>
827""")
828 manifest.projects
829
830 def test_extend_project_dest_path_multi_match_path_specified(self):
831 manifest = self.getXmlManifest("""
832<manifest>
833 <remote name="default-remote" fetch="http://localhost" />
834 <default remote="default-remote" revision="refs/heads/main" />
835 <project name="myproject" path="x" />
836 <project name="myproject" path="y" />
837 <extend-project name="myproject" path="x" dest-path="bar" />
838</manifest>
839""")
840 self.assertEqual(len(manifest.projects), 2)
841 if manifest.projects[0].relpath == 'y':
842 self.assertEqual(manifest.projects[1].relpath, 'bar')
843 else:
844 self.assertEqual(manifest.projects[0].relpath, 'bar')
845 self.assertEqual(manifest.projects[1].relpath, 'y')
diff --git a/tests/test_platform_utils.py b/tests/test_platform_utils.py
new file mode 100644
index 00000000..55b7805c
--- /dev/null
+++ b/tests/test_platform_utils.py
@@ -0,0 +1,50 @@
1# Copyright 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
17import os
18import tempfile
19import unittest
20
21import platform_utils
22
23
24class 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(OSError, platform_utils.remove, path, missing_ok=False)
38
39 # Should not fail if it exists.
40 open(path, 'w').close()
41 platform_utils.remove(path, missing_ok=True)
42 self.assertFalse(os.path.exists(path))
43
44 open(path, 'w').close()
45 platform_utils.remove(path)
46 self.assertFalse(os.path.exists(path))
47
48 open(path, 'w').close()
49 platform_utils.remove(path, missing_ok=False)
50 self.assertFalse(os.path.exists(path))
diff --git a/tests/test_project.py b/tests/test_project.py
index 77126dff..9b2cc4e9 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2019 The Android Open Source Project 1# Copyright (C) 2019 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,8 +14,6 @@
16 14
17"""Unittests for the project.py module.""" 15"""Unittests for the project.py module."""
18 16
19from __future__ import print_function
20
21import contextlib 17import contextlib
22import os 18import os
23import shutil 19import shutil
@@ -25,7 +21,10 @@ import subprocess
25import tempfile 21import tempfile
26import unittest 22import unittest
27 23
24import error
25import git_command
28import git_config 26import git_config
27import platform_utils
29import project 28import project
30 29
31 30
@@ -36,49 +35,22 @@ def TempGitTree():
36 # Python 2 support entirely. 35 # Python 2 support entirely.
37 try: 36 try:
38 tempdir = tempfile.mkdtemp(prefix='repo-tests') 37 tempdir = tempfile.mkdtemp(prefix='repo-tests')
39 subprocess.check_call(['git', 'init'], cwd=tempdir) 38
39 # Tests need to assume, that main is default branch at init,
40 # which is not supported in config until 2.28.
41 cmd = ['git', 'init']
42 if git_command.git_require((2, 28, 0)):
43 cmd += ['--initial-branch=main']
44 else:
45 # Use template dir for init.
46 templatedir = tempfile.mkdtemp(prefix='.test-template')
47 with open(os.path.join(templatedir, 'HEAD'), 'w') as fp:
48 fp.write('ref: refs/heads/main\n')
49 cmd += ['--template', templatedir]
50 subprocess.check_call(cmd, cwd=tempdir)
40 yield tempdir 51 yield tempdir
41 finally: 52 finally:
42 shutil.rmtree(tempdir) 53 platform_utils.rmtree(tempdir)
43
44
45class RepoHookShebang(unittest.TestCase):
46 """Check shebang parsing in RepoHook."""
47
48 def test_no_shebang(self):
49 """Lines w/out shebangs should be rejected."""
50 DATA = (
51 '',
52 '# -*- coding:utf-8 -*-\n',
53 '#\n# foo\n',
54 '# Bad shebang in script\n#!/foo\n'
55 )
56 for data in DATA:
57 self.assertIsNone(project.RepoHook._ExtractInterpFromShebang(data))
58
59 def test_direct_interp(self):
60 """Lines whose shebang points directly to the interpreter."""
61 DATA = (
62 ('#!/foo', '/foo'),
63 ('#! /foo', '/foo'),
64 ('#!/bin/foo ', '/bin/foo'),
65 ('#! /usr/foo ', '/usr/foo'),
66 ('#! /usr/foo -args', '/usr/foo'),
67 )
68 for shebang, interp in DATA:
69 self.assertEqual(project.RepoHook._ExtractInterpFromShebang(shebang),
70 interp)
71
72 def test_env_interp(self):
73 """Lines whose shebang launches through `env`."""
74 DATA = (
75 ('#!/usr/bin/env foo', 'foo'),
76 ('#!/bin/env foo', 'foo'),
77 ('#! /bin/env /bin/foo ', '/bin/foo'),
78 )
79 for shebang, interp in DATA:
80 self.assertEqual(project.RepoHook._ExtractInterpFromShebang(shebang),
81 interp)
82 54
83 55
84class FakeProject(object): 56class FakeProject(object):
@@ -114,7 +86,7 @@ class ReviewableBranchTests(unittest.TestCase):
114 86
115 # Start off with the normal details. 87 # Start off with the normal details.
116 rb = project.ReviewableBranch( 88 rb = project.ReviewableBranch(
117 fakeproj, fakeproj.config.GetBranch('work'), 'master') 89 fakeproj, fakeproj.config.GetBranch('work'), 'main')
118 self.assertEqual('work', rb.name) 90 self.assertEqual('work', rb.name)
119 self.assertEqual(1, len(rb.commits)) 91 self.assertEqual(1, len(rb.commits))
120 self.assertIn('Del file', rb.commits[0]) 92 self.assertIn('Del file', rb.commits[0])
@@ -127,10 +99,239 @@ class ReviewableBranchTests(unittest.TestCase):
127 self.assertTrue(rb.date) 99 self.assertTrue(rb.date)
128 100
129 # Now delete the tracking branch! 101 # Now delete the tracking branch!
130 fakeproj.work_git.branch('-D', 'master') 102 fakeproj.work_git.branch('-D', 'main')
131 rb = project.ReviewableBranch( 103 rb = project.ReviewableBranch(
132 fakeproj, fakeproj.config.GetBranch('work'), 'master') 104 fakeproj, fakeproj.config.GetBranch('work'), 'main')
133 self.assertEqual(0, len(rb.commits)) 105 self.assertEqual(0, len(rb.commits))
134 self.assertFalse(rb.base_exists) 106 self.assertFalse(rb.base_exists)
135 # Hard to assert anything useful about this. 107 # Hard to assert anything useful about this.
136 self.assertTrue(rb.date) 108 self.assertTrue(rb.date)
109
110
111class CopyLinkTestCase(unittest.TestCase):
112 """TestCase for stub repo client checkouts.
113
114 It'll have a layout like:
115 tempdir/ # self.tempdir
116 checkout/ # self.topdir
117 git-project/ # self.worktree
118
119 Attributes:
120 tempdir: A dedicated temporary directory.
121 worktree: The top of the repo client checkout.
122 topdir: The top of a project checkout.
123 """
124
125 def setUp(self):
126 self.tempdir = tempfile.mkdtemp(prefix='repo_tests')
127 self.topdir = os.path.join(self.tempdir, 'checkout')
128 self.worktree = os.path.join(self.topdir, 'git-project')
129 os.makedirs(self.topdir)
130 os.makedirs(self.worktree)
131
132 def tearDown(self):
133 shutil.rmtree(self.tempdir, ignore_errors=True)
134
135 @staticmethod
136 def touch(path):
137 with open(path, 'w'):
138 pass
139
140 def assertExists(self, path, msg=None):
141 """Make sure |path| exists."""
142 if os.path.exists(path):
143 return
144
145 if msg is None:
146 msg = ['path is missing: %s' % path]
147 while path != '/':
148 path = os.path.dirname(path)
149 if not path:
150 # If we're given something like "foo", abort once we get to "".
151 break
152 result = os.path.exists(path)
153 msg.append('\tos.path.exists(%s): %s' % (path, result))
154 if result:
155 msg.append('\tcontents: %r' % os.listdir(path))
156 break
157 msg = '\n'.join(msg)
158
159 raise self.failureException(msg)
160
161
162class CopyFile(CopyLinkTestCase):
163 """Check _CopyFile handling."""
164
165 def CopyFile(self, src, dest):
166 return project._CopyFile(self.worktree, src, self.topdir, dest)
167
168 def test_basic(self):
169 """Basic test of copying a file from a project to the toplevel."""
170 src = os.path.join(self.worktree, 'foo.txt')
171 self.touch(src)
172 cf = self.CopyFile('foo.txt', 'foo')
173 cf._Copy()
174 self.assertExists(os.path.join(self.topdir, 'foo'))
175
176 def test_src_subdir(self):
177 """Copy a file from a subdir of a project."""
178 src = os.path.join(self.worktree, 'bar', 'foo.txt')
179 os.makedirs(os.path.dirname(src))
180 self.touch(src)
181 cf = self.CopyFile('bar/foo.txt', 'new.txt')
182 cf._Copy()
183 self.assertExists(os.path.join(self.topdir, 'new.txt'))
184
185 def test_dest_subdir(self):
186 """Copy a file to a subdir of a checkout."""
187 src = os.path.join(self.worktree, 'foo.txt')
188 self.touch(src)
189 cf = self.CopyFile('foo.txt', 'sub/dir/new.txt')
190 self.assertFalse(os.path.exists(os.path.join(self.topdir, 'sub')))
191 cf._Copy()
192 self.assertExists(os.path.join(self.topdir, 'sub', 'dir', 'new.txt'))
193
194 def test_update(self):
195 """Make sure changed files get copied again."""
196 src = os.path.join(self.worktree, 'foo.txt')
197 dest = os.path.join(self.topdir, 'bar')
198 with open(src, 'w') as f:
199 f.write('1st')
200 cf = self.CopyFile('foo.txt', 'bar')
201 cf._Copy()
202 self.assertExists(dest)
203 with open(dest) as f:
204 self.assertEqual(f.read(), '1st')
205
206 with open(src, 'w') as f:
207 f.write('2nd!')
208 cf._Copy()
209 with open(dest) as f:
210 self.assertEqual(f.read(), '2nd!')
211
212 def test_src_block_symlink(self):
213 """Do not allow reading from a symlinked path."""
214 src = os.path.join(self.worktree, 'foo.txt')
215 sym = os.path.join(self.worktree, 'sym')
216 self.touch(src)
217 platform_utils.symlink('foo.txt', sym)
218 self.assertExists(sym)
219 cf = self.CopyFile('sym', 'foo')
220 self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
221
222 def test_src_block_symlink_traversal(self):
223 """Do not allow reading through a symlink dir."""
224 realfile = os.path.join(self.tempdir, 'file.txt')
225 self.touch(realfile)
226 src = os.path.join(self.worktree, 'bar', 'file.txt')
227 platform_utils.symlink(self.tempdir, os.path.join(self.worktree, 'bar'))
228 self.assertExists(src)
229 cf = self.CopyFile('bar/file.txt', 'foo')
230 self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
231
232 def test_src_block_copy_from_dir(self):
233 """Do not allow copying from a directory."""
234 src = os.path.join(self.worktree, 'dir')
235 os.makedirs(src)
236 cf = self.CopyFile('dir', 'foo')
237 self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
238
239 def test_dest_block_symlink(self):
240 """Do not allow writing to a symlink."""
241 src = os.path.join(self.worktree, 'foo.txt')
242 self.touch(src)
243 platform_utils.symlink('dest', os.path.join(self.topdir, 'sym'))
244 cf = self.CopyFile('foo.txt', 'sym')
245 self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
246
247 def test_dest_block_symlink_traversal(self):
248 """Do not allow writing through a symlink dir."""
249 src = os.path.join(self.worktree, 'foo.txt')
250 self.touch(src)
251 platform_utils.symlink(tempfile.gettempdir(),
252 os.path.join(self.topdir, 'sym'))
253 cf = self.CopyFile('foo.txt', 'sym/foo.txt')
254 self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
255
256 def test_src_block_copy_to_dir(self):
257 """Do not allow copying to a directory."""
258 src = os.path.join(self.worktree, 'foo.txt')
259 self.touch(src)
260 os.makedirs(os.path.join(self.topdir, 'dir'))
261 cf = self.CopyFile('foo.txt', 'dir')
262 self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
263
264
265class LinkFile(CopyLinkTestCase):
266 """Check _LinkFile handling."""
267
268 def LinkFile(self, src, dest):
269 return project._LinkFile(self.worktree, src, self.topdir, dest)
270
271 def test_basic(self):
272 """Basic test of linking a file from a project into the toplevel."""
273 src = os.path.join(self.worktree, 'foo.txt')
274 self.touch(src)
275 lf = self.LinkFile('foo.txt', 'foo')
276 lf._Link()
277 dest = os.path.join(self.topdir, 'foo')
278 self.assertExists(dest)
279 self.assertTrue(os.path.islink(dest))
280 self.assertEqual(os.path.join('git-project', 'foo.txt'), os.readlink(dest))
281
282 def test_src_subdir(self):
283 """Link to a file in a subdir of a project."""
284 src = os.path.join(self.worktree, 'bar', 'foo.txt')
285 os.makedirs(os.path.dirname(src))
286 self.touch(src)
287 lf = self.LinkFile('bar/foo.txt', 'foo')
288 lf._Link()
289 self.assertExists(os.path.join(self.topdir, 'foo'))
290
291 def test_src_self(self):
292 """Link to the project itself."""
293 dest = os.path.join(self.topdir, 'foo', 'bar')
294 lf = self.LinkFile('.', 'foo/bar')
295 lf._Link()
296 self.assertExists(dest)
297 self.assertEqual(os.path.join('..', 'git-project'), os.readlink(dest))
298
299 def test_dest_subdir(self):
300 """Link a file to a subdir of a checkout."""
301 src = os.path.join(self.worktree, 'foo.txt')
302 self.touch(src)
303 lf = self.LinkFile('foo.txt', 'sub/dir/foo/bar')
304 self.assertFalse(os.path.exists(os.path.join(self.topdir, 'sub')))
305 lf._Link()
306 self.assertExists(os.path.join(self.topdir, 'sub', 'dir', 'foo', 'bar'))
307
308 def test_src_block_relative(self):
309 """Do not allow relative symlinks."""
310 BAD_SOURCES = (
311 './',
312 '..',
313 '../',
314 'foo/.',
315 'foo/./bar',
316 'foo/..',
317 'foo/../foo',
318 )
319 for src in BAD_SOURCES:
320 lf = self.LinkFile(src, 'foo')
321 self.assertRaises(error.ManifestInvalidPathError, lf._Link)
322
323 def test_update(self):
324 """Make sure changed targets get updated."""
325 dest = os.path.join(self.topdir, 'sym')
326
327 src = os.path.join(self.worktree, 'foo.txt')
328 self.touch(src)
329 lf = self.LinkFile('foo.txt', 'sym')
330 lf._Link()
331 self.assertEqual(os.path.join('git-project', 'foo.txt'), os.readlink(dest))
332
333 # Point the symlink somewhere else.
334 os.unlink(dest)
335 platform_utils.symlink(self.tempdir, dest)
336 lf._Link()
337 self.assertEqual(os.path.join('git-project', 'foo.txt'), os.readlink(dest))
diff --git a/tests/test_ssh.py b/tests/test_ssh.py
new file mode 100644
index 00000000..ffb5cb94
--- /dev/null
+++ b/tests/test_ssh.py
@@ -0,0 +1,74 @@
1# Copyright 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
17import multiprocessing
18import subprocess
19import unittest
20from unittest import mock
21
22import ssh
23
24
25class 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('OpenSSH_6.6.1p1 Ubuntu-2ubuntu2.13, OpenSSL 1.0.1f 6 Jan 2014\n')
35 self.assertEqual(ver, (6, 6, 1))
36 ver = ssh._parse_ssh_version('OpenSSH_7.6p1 Ubuntu-4ubuntu0.3, OpenSSL 1.0.2n 7 Dec 2017\n')
37 self.assertEqual(ver, (7, 6))
38
39 def test_version(self):
40 """Check version() handling."""
41 with mock.patch('ssh._run_ssh_version', return_value='OpenSSH_1.2\n'):
42 self.assertEqual(ssh.version(), (1, 2))
43
44 def test_context_manager_empty(self):
45 """Verify context manager with no clients works correctly."""
46 with multiprocessing.Manager() as manager:
47 with ssh.ProxyManager(manager):
48 pass
49
50 def test_context_manager_child_cleanup(self):
51 """Verify orphaned clients & masters get cleaned up."""
52 with multiprocessing.Manager() as manager:
53 with ssh.ProxyManager(manager) as ssh_proxy:
54 client = subprocess.Popen(['sleep', '964853320'])
55 ssh_proxy.add_client(client)
56 master = subprocess.Popen(['sleep', '964853321'])
57 ssh_proxy.add_master(master)
58 # If the process still exists, these will throw timeout errors.
59 client.wait(0)
60 master.wait(0)
61
62 def test_ssh_sock(self):
63 """Check sock() function."""
64 manager = multiprocessing.Manager()
65 proxy = ssh.ProxyManager(manager)
66 with mock.patch('tempfile.mkdtemp', return_value='/tmp/foo'):
67 # old ssh version uses port
68 with mock.patch('ssh.version', return_value=(6, 6)):
69 self.assertTrue(proxy.sock().endswith('%p'))
70
71 proxy._sock_path = None
72 # new ssh version uses hash
73 with mock.patch('ssh.version', return_value=(6, 7)):
74 self.assertTrue(proxy.sock().endswith('%C'))
diff --git a/tests/test_subcmds.py b/tests/test_subcmds.py
new file mode 100644
index 00000000..bc53051a
--- /dev/null
+++ b/tests/test_subcmds.py
@@ -0,0 +1,73 @@
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 module (mostly __init__.py than subcommands)."""
16
17import optparse
18import unittest
19
20import subcmds
21
22
23class AllCommands(unittest.TestCase):
24 """Check registered all_commands."""
25
26 def test_required_basic(self):
27 """Basic checking of registered commands."""
28 # NB: We don't test all subcommands as we want to avoid "change detection"
29 # tests, so we just look for the most common/important ones here that are
30 # unlikely to ever change.
31 for cmd in {'cherry-pick', 'help', 'init', 'start', 'sync', 'upload'}:
32 self.assertIn(cmd, subcmds.all_commands)
33
34 def test_naming(self):
35 """Verify we don't add things that we shouldn't."""
36 for cmd in subcmds.all_commands:
37 # Reject filename suffixes like "help.py".
38 self.assertNotIn('.', cmd)
39
40 # Make sure all '_' were converted to '-'.
41 self.assertNotIn('_', cmd)
42
43 # Reject internal python paths like "__init__".
44 self.assertFalse(cmd.startswith('__'))
45
46 def test_help_desc_style(self):
47 """Force some consistency in option descriptions.
48
49 Python's optparse & argparse has a few default options like --help. Their
50 option description text uses lowercase sentence fragments, so enforce our
51 options follow the same style so UI is consistent.
52
53 We enforce:
54 * Text starts with lowercase.
55 * Text doesn't end with period.
56 """
57 for name, cls in subcmds.all_commands.items():
58 cmd = cls()
59 parser = cmd.OptionParser
60 for option in parser.option_list:
61 if option.help == optparse.SUPPRESS_HELP:
62 continue
63
64 c = option.help[0]
65 self.assertEqual(
66 c.lower(), c,
67 msg=f'subcmds/{name}.py: {option.get_opt_string()}: help text '
68 f'should start with lowercase: "{option.help}"')
69
70 self.assertNotEqual(
71 option.help[-1], '.',
72 msg=f'subcmds/{name}.py: {option.get_opt_string()}: help text '
73 f'should not end in a period: "{option.help}"')
diff --git a/tests/test_subcmds_init.py b/tests/test_subcmds_init.py
new file mode 100644
index 00000000..af4346de
--- /dev/null
+++ b/tests/test_subcmds_init.py
@@ -0,0 +1,49 @@
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
17import unittest
18
19from subcmds import init
20
21
22class 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 [],
32 )
33 for argv in ARGV:
34 opts, args = self.cmd.OptionParser.parse_args(argv)
35 self.cmd.ValidateOptions(opts, args)
36
37 def test_cli_parser_bad(self):
38 """Check invalid command line options."""
39 ARGV = (
40 # Too many arguments.
41 ['url', 'asdf'],
42
43 # Conflicting options.
44 ['--mirror', '--archive'],
45 )
46 for argv in ARGV:
47 opts, args = self.cmd.OptionParser.parse_args(argv)
48 with self.assertRaises(SystemExit):
49 self.cmd.ValidateOptions(opts, args)
diff --git a/tests/test_wrapper.py b/tests/test_wrapper.py
index 8ef8d48d..e9a1f64a 100644
--- a/tests/test_wrapper.py
+++ b/tests/test_wrapper.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2015 The Android Open Source Project 1# Copyright (C) 2015 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,27 +14,87 @@
16 14
17"""Unittests for the wrapper.py module.""" 15"""Unittests for the wrapper.py module."""
18 16
19from __future__ import print_function 17import contextlib
20 18from io import StringIO
21import os 19import os
20import re
21import shutil
22import sys
23import tempfile
22import unittest 24import unittest
25from unittest import mock
23 26
27import git_command
28import main
29import platform_utils
24import wrapper 30import wrapper
25 31
32
33@contextlib.contextmanager
34def TemporaryDirectory():
35 """Create a new empty git checkout for testing."""
36 # TODO(vapier): Convert this to tempfile.TemporaryDirectory once we drop
37 # Python 2 support entirely.
38 try:
39 tempdir = tempfile.mkdtemp(prefix='repo-tests')
40 yield tempdir
41 finally:
42 platform_utils.rmtree(tempdir)
43
44
26def fixture(*paths): 45def fixture(*paths):
27 """Return a path relative to tests/fixtures. 46 """Return a path relative to tests/fixtures.
28 """ 47 """
29 return os.path.join(os.path.dirname(__file__), 'fixtures', *paths) 48 return os.path.join(os.path.dirname(__file__), 'fixtures', *paths)
30 49
31class RepoWrapperUnitTest(unittest.TestCase): 50
32 """Tests helper functions in the repo wrapper 51class RepoWrapperTestCase(unittest.TestCase):
33 """ 52 """TestCase for the wrapper module."""
53
34 def setUp(self): 54 def setUp(self):
35 """Load the wrapper module every time 55 """Load the wrapper module every time."""
36 """
37 wrapper._wrapper_module = None 56 wrapper._wrapper_module = None
38 self.wrapper = wrapper.Wrapper() 57 self.wrapper = wrapper.Wrapper()
39 58
59
60class RepoWrapperUnitTest(RepoWrapperTestCase):
61 """Tests helper functions in the repo wrapper
62 """
63
64 def test_version(self):
65 """Make sure _Version works."""
66 with self.assertRaises(SystemExit) as e:
67 with mock.patch('sys.stdout', new_callable=StringIO) as stdout:
68 with mock.patch('sys.stderr', new_callable=StringIO) as stderr:
69 self.wrapper._Version()
70 self.assertEqual(0, e.exception.code)
71 self.assertEqual('', stderr.getvalue())
72 self.assertIn('repo launcher version', stdout.getvalue())
73
74 def test_python_constraints(self):
75 """The launcher should never require newer than main.py."""
76 self.assertGreaterEqual(main.MIN_PYTHON_VERSION_HARD,
77 wrapper.MIN_PYTHON_VERSION_HARD)
78 self.assertGreaterEqual(main.MIN_PYTHON_VERSION_SOFT,
79 wrapper.MIN_PYTHON_VERSION_SOFT)
80 # Make sure the versions are themselves in sync.
81 self.assertGreaterEqual(wrapper.MIN_PYTHON_VERSION_SOFT,
82 wrapper.MIN_PYTHON_VERSION_HARD)
83
84 def test_init_parser(self):
85 """Make sure 'init' GetParser works."""
86 parser = self.wrapper.GetParser(gitc_init=False)
87 opts, args = parser.parse_args([])
88 self.assertEqual([], args)
89 self.assertIsNone(opts.manifest_url)
90
91 def test_gitc_init_parser(self):
92 """Make sure 'gitc-init' GetParser works."""
93 parser = self.wrapper.GetParser(gitc_init=True)
94 opts, args = parser.parse_args([])
95 self.assertEqual([], args)
96 self.assertIsNone(opts.manifest_file)
97
40 def test_get_gitc_manifest_dir_no_gitc(self): 98 def test_get_gitc_manifest_dir_no_gitc(self):
41 """ 99 """
42 Test reading a missing gitc config file 100 Test reading a missing gitc config file
@@ -72,9 +130,442 @@ class RepoWrapperUnitTest(unittest.TestCase):
72 self.assertEqual(self.wrapper.gitc_parse_clientdir('/gitc/manifest-rw/test/extra'), 'test') 130 self.assertEqual(self.wrapper.gitc_parse_clientdir('/gitc/manifest-rw/test/extra'), 'test')
73 self.assertEqual(self.wrapper.gitc_parse_clientdir('/test/usr/local/google/gitc/test'), 'test') 131 self.assertEqual(self.wrapper.gitc_parse_clientdir('/test/usr/local/google/gitc/test'), 'test')
74 self.assertEqual(self.wrapper.gitc_parse_clientdir('/test/usr/local/google/gitc/test/'), 'test') 132 self.assertEqual(self.wrapper.gitc_parse_clientdir('/test/usr/local/google/gitc/test/'), 'test')
75 self.assertEqual(self.wrapper.gitc_parse_clientdir('/test/usr/local/google/gitc/test/extra'), 'test') 133 self.assertEqual(self.wrapper.gitc_parse_clientdir('/test/usr/local/google/gitc/test/extra'),
134 'test')
76 self.assertEqual(self.wrapper.gitc_parse_clientdir('/gitc/manifest-rw/'), None) 135 self.assertEqual(self.wrapper.gitc_parse_clientdir('/gitc/manifest-rw/'), None)
77 self.assertEqual(self.wrapper.gitc_parse_clientdir('/test/usr/local/google/gitc/'), None) 136 self.assertEqual(self.wrapper.gitc_parse_clientdir('/test/usr/local/google/gitc/'), None)
78 137
138
139class SetGitTrace2ParentSid(RepoWrapperTestCase):
140 """Check SetGitTrace2ParentSid behavior."""
141
142 KEY = 'GIT_TRACE2_PARENT_SID'
143 VALID_FORMAT = re.compile(r'^repo-[0-9]{8}T[0-9]{6}Z-P[0-9a-f]{8}$')
144
145 def test_first_set(self):
146 """Test env var not yet set."""
147 env = {}
148 self.wrapper.SetGitTrace2ParentSid(env)
149 self.assertIn(self.KEY, env)
150 value = env[self.KEY]
151 self.assertRegex(value, self.VALID_FORMAT)
152
153 def test_append(self):
154 """Test env var is appended."""
155 env = {self.KEY: 'pfx'}
156 self.wrapper.SetGitTrace2ParentSid(env)
157 self.assertIn(self.KEY, env)
158 value = env[self.KEY]
159 self.assertTrue(value.startswith('pfx/'))
160 self.assertRegex(value[4:], self.VALID_FORMAT)
161
162 def test_global_context(self):
163 """Check os.environ gets updated by default."""
164 os.environ.pop(self.KEY, None)
165 self.wrapper.SetGitTrace2ParentSid()
166 self.assertIn(self.KEY, os.environ)
167 value = os.environ[self.KEY]
168 self.assertRegex(value, self.VALID_FORMAT)
169
170
171class RunCommand(RepoWrapperTestCase):
172 """Check run_command behavior."""
173
174 def test_capture(self):
175 """Check capture_output handling."""
176 ret = self.wrapper.run_command(['echo', 'hi'], capture_output=True)
177 self.assertEqual(ret.stdout, 'hi\n')
178
179 def test_check(self):
180 """Check check handling."""
181 self.wrapper.run_command(['true'], check=False)
182 self.wrapper.run_command(['true'], check=True)
183 self.wrapper.run_command(['false'], check=False)
184 with self.assertRaises(self.wrapper.RunError):
185 self.wrapper.run_command(['false'], check=True)
186
187
188class RunGit(RepoWrapperTestCase):
189 """Check run_git behavior."""
190
191 def test_capture(self):
192 """Check capture_output handling."""
193 ret = self.wrapper.run_git('--version')
194 self.assertIn('git', ret.stdout)
195
196 def test_check(self):
197 """Check check handling."""
198 with self.assertRaises(self.wrapper.CloneFailure):
199 self.wrapper.run_git('--version-asdfasdf')
200 self.wrapper.run_git('--version-asdfasdf', check=False)
201
202
203class ParseGitVersion(RepoWrapperTestCase):
204 """Check ParseGitVersion behavior."""
205
206 def test_autoload(self):
207 """Check we can load the version from the live git."""
208 ret = self.wrapper.ParseGitVersion()
209 self.assertIsNotNone(ret)
210
211 def test_bad_ver(self):
212 """Check handling of bad git versions."""
213 ret = self.wrapper.ParseGitVersion(ver_str='asdf')
214 self.assertIsNone(ret)
215
216 def test_normal_ver(self):
217 """Check handling of normal git versions."""
218 ret = self.wrapper.ParseGitVersion(ver_str='git version 2.25.1')
219 self.assertEqual(2, ret.major)
220 self.assertEqual(25, ret.minor)
221 self.assertEqual(1, ret.micro)
222 self.assertEqual('2.25.1', ret.full)
223
224 def test_extended_ver(self):
225 """Check handling of extended distro git versions."""
226 ret = self.wrapper.ParseGitVersion(
227 ver_str='git version 1.30.50.696.g5e7596f4ac-goog')
228 self.assertEqual(1, ret.major)
229 self.assertEqual(30, ret.minor)
230 self.assertEqual(50, ret.micro)
231 self.assertEqual('1.30.50.696.g5e7596f4ac-goog', ret.full)
232
233
234class CheckGitVersion(RepoWrapperTestCase):
235 """Check _CheckGitVersion behavior."""
236
237 def test_unknown(self):
238 """Unknown versions should abort."""
239 with mock.patch.object(self.wrapper, 'ParseGitVersion', return_value=None):
240 with self.assertRaises(self.wrapper.CloneFailure):
241 self.wrapper._CheckGitVersion()
242
243 def test_old(self):
244 """Old versions should abort."""
245 with mock.patch.object(
246 self.wrapper, 'ParseGitVersion',
247 return_value=self.wrapper.GitVersion(1, 0, 0, '1.0.0')):
248 with self.assertRaises(self.wrapper.CloneFailure):
249 self.wrapper._CheckGitVersion()
250
251 def test_new(self):
252 """Newer versions should run fine."""
253 with mock.patch.object(
254 self.wrapper, 'ParseGitVersion',
255 return_value=self.wrapper.GitVersion(100, 0, 0, '100.0.0')):
256 self.wrapper._CheckGitVersion()
257
258
259class Requirements(RepoWrapperTestCase):
260 """Check Requirements handling."""
261
262 def test_missing_file(self):
263 """Don't crash if the file is missing (old version)."""
264 testdir = os.path.dirname(os.path.realpath(__file__))
265 self.assertIsNone(self.wrapper.Requirements.from_dir(testdir))
266 self.assertIsNone(self.wrapper.Requirements.from_file(
267 os.path.join(testdir, 'xxxxxxxxxxxxxxxxxxxxxxxx')))
268
269 def test_corrupt_data(self):
270 """If the file can't be parsed, don't blow up."""
271 self.assertIsNone(self.wrapper.Requirements.from_file(__file__))
272 self.assertIsNone(self.wrapper.Requirements.from_data(b'x'))
273
274 def test_valid_data(self):
275 """Make sure we can parse the file we ship."""
276 self.assertIsNotNone(self.wrapper.Requirements.from_data(b'{}'))
277 rootdir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
278 self.assertIsNotNone(self.wrapper.Requirements.from_dir(rootdir))
279 self.assertIsNotNone(self.wrapper.Requirements.from_file(os.path.join(
280 rootdir, 'requirements.json')))
281
282 def test_format_ver(self):
283 """Check format_ver can format."""
284 self.assertEqual('1.2.3', self.wrapper.Requirements._format_ver((1, 2, 3)))
285 self.assertEqual('1', self.wrapper.Requirements._format_ver([1]))
286
287 def test_assert_all_unknown(self):
288 """Check assert_all works with incompatible file."""
289 reqs = self.wrapper.Requirements({})
290 reqs.assert_all()
291
292 def test_assert_all_new_repo(self):
293 """Check assert_all accepts new enough repo."""
294 reqs = self.wrapper.Requirements({'repo': {'hard': [1, 0]}})
295 reqs.assert_all()
296
297 def test_assert_all_old_repo(self):
298 """Check assert_all rejects old repo."""
299 reqs = self.wrapper.Requirements({'repo': {'hard': [99999, 0]}})
300 with self.assertRaises(SystemExit):
301 reqs.assert_all()
302
303 def test_assert_all_new_python(self):
304 """Check assert_all accepts new enough python."""
305 reqs = self.wrapper.Requirements({'python': {'hard': sys.version_info}})
306 reqs.assert_all()
307
308 def test_assert_all_old_python(self):
309 """Check assert_all rejects old python."""
310 reqs = self.wrapper.Requirements({'python': {'hard': [99999, 0]}})
311 with self.assertRaises(SystemExit):
312 reqs.assert_all()
313
314 def test_assert_ver_unknown(self):
315 """Check assert_ver works with incompatible file."""
316 reqs = self.wrapper.Requirements({})
317 reqs.assert_ver('xxx', (1, 0))
318
319 def test_assert_ver_new(self):
320 """Check assert_ver allows new enough versions."""
321 reqs = self.wrapper.Requirements({'git': {'hard': [1, 0], 'soft': [2, 0]}})
322 reqs.assert_ver('git', (1, 0))
323 reqs.assert_ver('git', (1, 5))
324 reqs.assert_ver('git', (2, 0))
325 reqs.assert_ver('git', (2, 5))
326
327 def test_assert_ver_old(self):
328 """Check assert_ver rejects old versions."""
329 reqs = self.wrapper.Requirements({'git': {'hard': [1, 0], 'soft': [2, 0]}})
330 with self.assertRaises(SystemExit):
331 reqs.assert_ver('git', (0, 5))
332
333
334class NeedSetupGnuPG(RepoWrapperTestCase):
335 """Check NeedSetupGnuPG behavior."""
336
337 def test_missing_dir(self):
338 """The ~/.repoconfig tree doesn't exist yet."""
339 with TemporaryDirectory() as tempdir:
340 self.wrapper.home_dot_repo = os.path.join(tempdir, 'foo')
341 self.assertTrue(self.wrapper.NeedSetupGnuPG())
342
343 def test_missing_keyring(self):
344 """The keyring-version file doesn't exist yet."""
345 with TemporaryDirectory() as tempdir:
346 self.wrapper.home_dot_repo = tempdir
347 self.assertTrue(self.wrapper.NeedSetupGnuPG())
348
349 def test_empty_keyring(self):
350 """The keyring-version file exists, but is empty."""
351 with TemporaryDirectory() as tempdir:
352 self.wrapper.home_dot_repo = tempdir
353 with open(os.path.join(tempdir, 'keyring-version'), 'w'):
354 pass
355 self.assertTrue(self.wrapper.NeedSetupGnuPG())
356
357 def test_old_keyring(self):
358 """The keyring-version file exists, but it's old."""
359 with TemporaryDirectory() as tempdir:
360 self.wrapper.home_dot_repo = tempdir
361 with open(os.path.join(tempdir, 'keyring-version'), 'w') as fp:
362 fp.write('1.0\n')
363 self.assertTrue(self.wrapper.NeedSetupGnuPG())
364
365 def test_new_keyring(self):
366 """The keyring-version file exists, and is up-to-date."""
367 with TemporaryDirectory() as tempdir:
368 self.wrapper.home_dot_repo = tempdir
369 with open(os.path.join(tempdir, 'keyring-version'), 'w') as fp:
370 fp.write('1000.0\n')
371 self.assertFalse(self.wrapper.NeedSetupGnuPG())
372
373
374class SetupGnuPG(RepoWrapperTestCase):
375 """Check SetupGnuPG behavior."""
376
377 def test_full(self):
378 """Make sure it works completely."""
379 with TemporaryDirectory() as tempdir:
380 self.wrapper.home_dot_repo = tempdir
381 self.wrapper.gpg_dir = os.path.join(self.wrapper.home_dot_repo, 'gnupg')
382 self.assertTrue(self.wrapper.SetupGnuPG(True))
383 with open(os.path.join(tempdir, 'keyring-version'), 'r') as fp:
384 data = fp.read()
385 self.assertEqual('.'.join(str(x) for x in self.wrapper.KEYRING_VERSION),
386 data.strip())
387
388
389class VerifyRev(RepoWrapperTestCase):
390 """Check verify_rev behavior."""
391
392 def test_verify_passes(self):
393 """Check when we have a valid signed tag."""
394 desc_result = self.wrapper.RunResult(0, 'v1.0\n', '')
395 gpg_result = self.wrapper.RunResult(0, '', '')
396 with mock.patch.object(self.wrapper, 'run_git',
397 side_effect=(desc_result, gpg_result)):
398 ret = self.wrapper.verify_rev('/', 'refs/heads/stable', '1234', True)
399 self.assertEqual('v1.0^0', ret)
400
401 def test_unsigned_commit(self):
402 """Check we fall back to signed tag when we have an unsigned commit."""
403 desc_result = self.wrapper.RunResult(0, 'v1.0-10-g1234\n', '')
404 gpg_result = self.wrapper.RunResult(0, '', '')
405 with mock.patch.object(self.wrapper, 'run_git',
406 side_effect=(desc_result, gpg_result)):
407 ret = self.wrapper.verify_rev('/', 'refs/heads/stable', '1234', True)
408 self.assertEqual('v1.0^0', ret)
409
410 def test_verify_fails(self):
411 """Check we fall back to signed tag when we have an unsigned commit."""
412 desc_result = self.wrapper.RunResult(0, 'v1.0-10-g1234\n', '')
413 gpg_result = Exception
414 with mock.patch.object(self.wrapper, 'run_git',
415 side_effect=(desc_result, gpg_result)):
416 with self.assertRaises(Exception):
417 self.wrapper.verify_rev('/', 'refs/heads/stable', '1234', True)
418
419
420class GitCheckoutTestCase(RepoWrapperTestCase):
421 """Tests that use a real/small git checkout."""
422
423 GIT_DIR = None
424 REV_LIST = None
425
426 @classmethod
427 def setUpClass(cls):
428 # Create a repo to operate on, but do it once per-class.
429 cls.GIT_DIR = tempfile.mkdtemp(prefix='repo-rev-tests')
430 run_git = wrapper.Wrapper().run_git
431
432 remote = os.path.join(cls.GIT_DIR, 'remote')
433 os.mkdir(remote)
434
435 # Tests need to assume, that main is default branch at init,
436 # which is not supported in config until 2.28.
437 if git_command.git_require((2, 28, 0)):
438 initstr = '--initial-branch=main'
439 else:
440 # Use template dir for init.
441 templatedir = tempfile.mkdtemp(prefix='.test-template')
442 with open(os.path.join(templatedir, 'HEAD'), 'w') as fp:
443 fp.write('ref: refs/heads/main\n')
444 initstr = '--template=' + templatedir
445
446 run_git('init', initstr, cwd=remote)
447 run_git('commit', '--allow-empty', '-minit', cwd=remote)
448 run_git('branch', 'stable', cwd=remote)
449 run_git('tag', 'v1.0', cwd=remote)
450 run_git('commit', '--allow-empty', '-m2nd commit', cwd=remote)
451 cls.REV_LIST = run_git('rev-list', 'HEAD', cwd=remote).stdout.splitlines()
452
453 run_git('init', cwd=cls.GIT_DIR)
454 run_git('fetch', remote, '+refs/heads/*:refs/remotes/origin/*', cwd=cls.GIT_DIR)
455
456 @classmethod
457 def tearDownClass(cls):
458 if not cls.GIT_DIR:
459 return
460
461 shutil.rmtree(cls.GIT_DIR)
462
463
464class ResolveRepoRev(GitCheckoutTestCase):
465 """Check resolve_repo_rev behavior."""
466
467 def test_explicit_branch(self):
468 """Check refs/heads/branch argument."""
469 rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, 'refs/heads/stable')
470 self.assertEqual('refs/heads/stable', rrev)
471 self.assertEqual(self.REV_LIST[1], lrev)
472
473 with self.assertRaises(wrapper.CloneFailure):
474 self.wrapper.resolve_repo_rev(self.GIT_DIR, 'refs/heads/unknown')
475
476 def test_explicit_tag(self):
477 """Check refs/tags/tag argument."""
478 rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, 'refs/tags/v1.0')
479 self.assertEqual('refs/tags/v1.0', rrev)
480 self.assertEqual(self.REV_LIST[1], lrev)
481
482 with self.assertRaises(wrapper.CloneFailure):
483 self.wrapper.resolve_repo_rev(self.GIT_DIR, 'refs/tags/unknown')
484
485 def test_branch_name(self):
486 """Check branch argument."""
487 rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, 'stable')
488 self.assertEqual('refs/heads/stable', rrev)
489 self.assertEqual(self.REV_LIST[1], lrev)
490
491 rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, 'main')
492 self.assertEqual('refs/heads/main', rrev)
493 self.assertEqual(self.REV_LIST[0], lrev)
494
495 def test_tag_name(self):
496 """Check tag argument."""
497 rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, 'v1.0')
498 self.assertEqual('refs/tags/v1.0', rrev)
499 self.assertEqual(self.REV_LIST[1], lrev)
500
501 def test_full_commit(self):
502 """Check specific commit argument."""
503 commit = self.REV_LIST[0]
504 rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, commit)
505 self.assertEqual(commit, rrev)
506 self.assertEqual(commit, lrev)
507
508 def test_partial_commit(self):
509 """Check specific (partial) commit argument."""
510 commit = self.REV_LIST[0][0:20]
511 rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, commit)
512 self.assertEqual(self.REV_LIST[0], rrev)
513 self.assertEqual(self.REV_LIST[0], lrev)
514
515 def test_unknown(self):
516 """Check unknown ref/commit argument."""
517 with self.assertRaises(wrapper.CloneFailure):
518 self.wrapper.resolve_repo_rev(self.GIT_DIR, 'boooooooya')
519
520
521class CheckRepoVerify(RepoWrapperTestCase):
522 """Check check_repo_verify behavior."""
523
524 def test_no_verify(self):
525 """Always fail with --no-repo-verify."""
526 self.assertFalse(self.wrapper.check_repo_verify(False))
527
528 def test_gpg_initialized(self):
529 """Should pass if gpg is setup already."""
530 with mock.patch.object(self.wrapper, 'NeedSetupGnuPG', return_value=False):
531 self.assertTrue(self.wrapper.check_repo_verify(True))
532
533 def test_need_gpg_setup(self):
534 """Should pass/fail based on gpg setup."""
535 with mock.patch.object(self.wrapper, 'NeedSetupGnuPG', return_value=True):
536 with mock.patch.object(self.wrapper, 'SetupGnuPG') as m:
537 m.return_value = True
538 self.assertTrue(self.wrapper.check_repo_verify(True))
539
540 m.return_value = False
541 self.assertFalse(self.wrapper.check_repo_verify(True))
542
543
544class CheckRepoRev(GitCheckoutTestCase):
545 """Check check_repo_rev behavior."""
546
547 def test_verify_works(self):
548 """Should pass when verification passes."""
549 with mock.patch.object(self.wrapper, 'check_repo_verify', return_value=True):
550 with mock.patch.object(self.wrapper, 'verify_rev', return_value='12345'):
551 rrev, lrev = self.wrapper.check_repo_rev(self.GIT_DIR, 'stable')
552 self.assertEqual('refs/heads/stable', rrev)
553 self.assertEqual('12345', lrev)
554
555 def test_verify_fails(self):
556 """Should fail when verification fails."""
557 with mock.patch.object(self.wrapper, 'check_repo_verify', return_value=True):
558 with mock.patch.object(self.wrapper, 'verify_rev', side_effect=Exception):
559 with self.assertRaises(Exception):
560 self.wrapper.check_repo_rev(self.GIT_DIR, 'stable')
561
562 def test_verify_ignore(self):
563 """Should pass when verification is disabled."""
564 with mock.patch.object(self.wrapper, 'verify_rev', side_effect=Exception):
565 rrev, lrev = self.wrapper.check_repo_rev(self.GIT_DIR, 'stable', repo_verify=False)
566 self.assertEqual('refs/heads/stable', rrev)
567 self.assertEqual(self.REV_LIST[1], lrev)
568
569
79if __name__ == '__main__': 570if __name__ == '__main__':
80 unittest.main() 571 unittest.main()
diff --git a/tox.ini b/tox.ini
index 02c5647d..aa4e2979 100644
--- a/tox.ini
+++ b/tox.ini
@@ -15,8 +15,19 @@
15# https://tox.readthedocs.io/ 15# https://tox.readthedocs.io/
16 16
17[tox] 17[tox]
18envlist = py27, py36, py37, py38 18envlist = py36, py37, py38, py39
19
20[gh-actions]
21python =
22 3.6: py36
23 3.7: py37
24 3.8: py38
25 3.9: py39
19 26
20[testenv] 27[testenv]
21deps = pytest 28deps = pytest
22commands = {toxinidir}/run_tests 29commands = {envpython} run_tests
30setenv =
31 GIT_AUTHOR_NAME = Repo test author
32 GIT_COMMITTER_NAME = Repo test committer
33 EMAIL = repo@gerrit.nodomain
diff --git a/wrapper.py b/wrapper.py
index 0ce32508..b1aa4c50 100644
--- a/wrapper.py
+++ b/wrapper.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2014 The Android Open Source Project 1# Copyright (C) 2014 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,7 +12,6 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function
18try: 15try:
19 from importlib.machinery import SourceFileLoader 16 from importlib.machinery import SourceFileLoader
20 _loader = lambda *args: SourceFileLoader(*args).load_module() 17 _loader = lambda *args: SourceFileLoader(*args).load_module()
@@ -27,7 +24,10 @@ import os
27def WrapperPath(): 24def WrapperPath():
28 return os.path.join(os.path.dirname(__file__), 'repo') 25 return os.path.join(os.path.dirname(__file__), 'repo')
29 26
27
30_wrapper_module = None 28_wrapper_module = None
29
30
31def Wrapper(): 31def Wrapper():
32 global _wrapper_module 32 global _wrapper_module
33 if not _wrapper_module: 33 if not _wrapper_module: