diff options
120 files changed, 13197 insertions, 3816 deletions
@@ -1,3 +1,15 @@ | |||
1 | [flake8] | 1 | [flake8] |
2 | max-line-length=80 | 2 | max-line-length=100 |
3 | ignore=E111,E114,E402 | 3 | ignore= |
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 | |||
4 | name: Test CI | ||
5 | |||
6 | on: | ||
7 | push: | ||
8 | branches: [main, repo-1, stable, maint] | ||
9 | tags: [v*] | ||
10 | |||
11 | jobs: | ||
12 | test: | ||
13 | strategy: | ||
14 | fail-fast: false | ||
15 | matrix: | ||
16 | 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 | ||
@@ -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/ |
@@ -4,6 +4,7 @@ Hu Xiuyun <xiuyun.hu@hisilicon.com> Hu xiuyun <xiuyun.hu@hisilicon.com | |||
4 | Hu Xiuyun <xiuyun.hu@hisilicon.com> Hu Xiuyun <clouds08@qq.com> | 4 | Hu Xiuyun <xiuyun.hu@hisilicon.com> Hu Xiuyun <clouds08@qq.com> |
5 | Jelly Chen <chenguodong@huawei.com> chenguodong <chenguodong@huawei.com> | 5 | Jelly Chen <chenguodong@huawei.com> chenguodong <chenguodong@huawei.com> |
6 | Jia Bi <bijia@xiaomi.com> bijia <bijia@xiaomi.com> | 6 | Jia Bi <bijia@xiaomi.com> bijia <bijia@xiaomi.com> |
7 | Jiri Tyr <jiri.tyr@gmail.com> Jiri tyr <jiri.tyr@gmail.com> | ||
7 | JoonCheol Park <jooncheol@gmail.com> Jooncheol Park <jooncheol@gmail.com> | 8 | JoonCheol Park <jooncheol@gmail.com> Jooncheol Park <jooncheol@gmail.com> |
8 | Sergii Pylypenko <x.pelya.x@gmail.com> pelya <x.pelya.x@gmail.com> | 9 | Sergii Pylypenko <x.pelya.x@gmail.com> pelya <x.pelya.x@gmail.com> |
9 | Shawn Pearce <sop@google.com> Shawn O. Pearce <sop@google.com> | 10 | Shawn Pearce <sop@google.com> Shawn O. Pearce <sop@google.com> |
@@ -6,15 +6,29 @@ development workflow. Repo is not meant to replace Git, only to make it | |||
6 | easier to work with Git. The repo command is an executable Python script | 6 | easier to work with Git. The repo command is an executable Python script |
7 | that you can put anywhere in your path. | 7 | that 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 | |||
24 | Please use the [repo-discuss] mailing list or [issue tracker] for questions. | ||
25 | |||
26 | You can [file a new bug report][new-bug] under the "repo" component. | ||
27 | |||
28 | Please do not e-mail individual developers for support. | ||
29 | They do not have the bandwidth for it, and often times questions have already | ||
30 | been asked on [repo-discuss] or bugs posted to the [issue tracker]. | ||
31 | So please search those sites first. | ||
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 | |||
38 | probably need to split up your commit to finer grained pieces. | 38 | probably 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 | 43 | Run `flake8` on changed modules: |
44 | 44 | ||
45 | Run `pyflakes` on changed modules: | 45 | flake8 file.py |
46 | |||
47 | pyflakes file.py | ||
48 | 46 | ||
49 | Ideally there should be no new errors or warnings introduced. | 47 | Note 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 | ||
53 | Run `flake8` on changes modules: | 53 | There should be no new errors or warnings introduced. |
54 | 54 | ||
55 | flake8 file.py | 55 | Warnings that cannot be avoided without going against the Google Style Guide |
56 | may be suppressed inline individally using a `# noqa` comment as described | ||
57 | in the [flake8 documentation]. | ||
56 | 58 | ||
57 | Note that repo generally follows [Google's python style guide] rather than | 59 | If 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. | 60 | the entire project in the included `.flake8` file. |
59 | It's not mandatory to avoid all warnings, but at least the maximum line | ||
60 | length should be followed. | ||
61 | 61 | ||
62 | If there are many occurrences of the same warning that cannot be | 62 | [Google's Python Style Guide]: https://google.github.io/styleguide/pyguide.html |
63 | avoided without going against the Google style guide, these may be | ||
64 | suppressed 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 | |||
154 | a remembered remote to make this easier in the future: | 150 | a 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 | ||
@@ -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 | |||
87 | DEFAULT = None | 86 | DEFAULT = None |
88 | 87 | ||
89 | 88 | ||
@@ -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 | ||
15 | import multiprocessing | ||
17 | import os | 16 | import os |
18 | import optparse | 17 | import optparse |
19 | import platform | ||
20 | import re | 18 | import re |
21 | import sys | 19 | import sys |
22 | 20 | ||
23 | from event_log import EventLog | 21 | from event_log import EventLog |
24 | from error import NoSuchProjectError | 22 | from error import NoSuchProjectError |
25 | from error import InvalidProjectGroupsError | 23 | from error import InvalidProjectGroupsError |
24 | import progress | ||
25 | |||
26 | |||
27 | # Are we generating man-pages? | ||
28 | GENERATE_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. | ||
37 | WORKER_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. | ||
42 | DEFAULT_LOCAL_JOBS = min(os.cpu_count(), 8) | ||
26 | 43 | ||
27 | 44 | ||
28 | class Command(object): | 45 | class 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. | ||
156 | complete -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 | |||
3 | A reference to the `.repo/` tree in repo client checkouts. | ||
4 | Hopefully it's complete & up-to-date, but who knows! | ||
5 | |||
6 | *** note | ||
7 | **Warning**: | ||
8 | This 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 | ||
10 | change its internals in backwards incompatible ways. | ||
11 | *** | ||
12 | |||
13 | [TOC] | ||
14 | |||
15 | ## .repo/ layout | ||
16 | |||
17 | All content under `.repo/` is managed by `repo` itself with few exceptions. | ||
18 | |||
19 | In general, you should not make manual changes in here. | ||
20 | If a setting was initialized using an option to `repo init`, you should use that | ||
21 | command to change the setting later on. | ||
22 | It is always safe to re-run `repo init` in existing repo client checkouts. | ||
23 | For 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 | |||
50 | For more documentation on the manifest format, including the local_manifests | ||
51 | support, 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/ | ||
98 | layouts as a model for other tools to implement similar approaches. | ||
99 | It 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 | |||
109 | Instead, 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 | |||
147 | The `.repo/manifests.git/config` file is used to track settings for the entire | ||
148 | repo client checkout. | ||
149 | |||
150 | Most settings use the `[repo]` section to avoid conflicts with git. | ||
151 | |||
152 | Everything under `[repo.syncstate.*]` is used to keep track of sync details for logging | ||
153 | purposes. | ||
154 | |||
155 | User 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 | |||
181 | For more details on this feature, see the [repo-hooks docs](./repo-hooks.md). | ||
182 | We'll just discuss the internal configuration settings. | ||
183 | These are stored in the registered `<repo-hooks>` project itself, so if the | ||
184 | manifest 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 | |||
192 | For example, if our manifest had the following entries, we would store settings | ||
193 | under `.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 | |||
200 | If `<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 | |||
208 | These settings are somewhat meant to be tweaked by the user on a per-project | ||
209 | basis (e.g. `git config` in a checked out source repo). | ||
210 | |||
211 | Where possible, we re-use standard git settings to avoid confusion, and we | ||
212 | refrain from documenting those, so see [git-config] documentation instead. | ||
213 | |||
214 | See `repo help upload` for documentation on `[review]` settings. | ||
215 | |||
216 | The `[remote]` settings are automatically populated/updated from the manifest. | ||
217 | |||
218 | The `[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 | |||
240 | Repo 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 | ||
120 | For compatibility purposes across repo releases, all unknown elements are | ||
121 | silently ignored. However, repo reserves all possible names for itself for | ||
122 | future use. If you want to use custom elements, the `x-*` namespace is | ||
123 | reserved for that purpose, and repo guarantees to never allocate any | ||
124 | corresponding names. | ||
125 | |||
105 | A description of the elements and their attributes follows. | 126 | A 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 | ||
110 | The root element of the file. | 131 | The root element of the file. |
111 | 132 | ||
133 | ### Element notice | ||
134 | |||
135 | Arbitrary text that is displayed to users whenever `repo sync` finishes. | ||
136 | The 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 | |||
141 | are uploaded to by `repo upload`. This attribute is optional; | 166 | are uploaded to by `repo upload`. This attribute is optional; |
142 | if not specified then `repo upload` will not function. | 167 | if not specified then `repo upload` will not function. |
143 | 168 | ||
144 | Attribute `revision`: Name of a Git branch (e.g. `master` or | 169 | Attribute `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 |
146 | the default revision. | 171 | the default revision. |
147 | 172 | ||
148 | ### Element default | 173 | ### Element default |
@@ -155,11 +180,11 @@ Attribute `remote`: Name of a previously defined remote element. | |||
155 | Project elements lacking a remote attribute of their own will use | 180 | Project elements lacking a remote attribute of their own will use |
156 | this remote. | 181 | this remote. |
157 | 182 | ||
158 | Attribute `revision`: Name of a Git branch (e.g. `master` or | 183 | Attribute `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 |
160 | revision attribute will use this revision. | 185 | revision attribute will use this revision. |
161 | 186 | ||
162 | Attribute `dest-branch`: Name of a Git branch (e.g. `master`). | 187 | Attribute `dest-branch`: Name of a Git branch (e.g. `main`). |
163 | Project elements not setting their own `dest-branch` will inherit | 188 | Project elements not setting their own `dest-branch` will inherit |
164 | this value. If this value is not set, projects will use `revision` | 189 | this value. If this value is not set, projects will use `revision` |
165 | by default instead. | 190 | by default instead. |
@@ -235,24 +260,37 @@ name will be prefixed by the parent's. | |||
235 | The project name must match the name Gerrit knows, if Gerrit is | 260 | The project name must match the name Gerrit knows, if Gerrit is |
236 | being used for code reviews. | 261 | being used for code reviews. |
237 | 262 | ||
263 | "name" must not be empty, and may not be an absolute path or use "." or ".." | ||
264 | path components. It is always interpreted relative to the remote's fetch | ||
265 | settings, so if a different base path is needed, declare a different remote | ||
266 | with the new settings needed. | ||
267 | These restrictions are not enforced for [Local Manifests]. | ||
268 | |||
238 | Attribute `path`: An optional path relative to the top directory | 269 | Attribute `path`: An optional path relative to the top directory |
239 | of the repo client where the Git working directory for this project | 270 | of the repo client where the Git working directory for this project |
240 | should be placed. If not supplied the project name is used. | 271 | should be placed. If not supplied the project "name" is used. |
241 | If the project has a parent element, its path will be prefixed | 272 | If the project has a parent element, its path will be prefixed |
242 | by the parent's. | 273 | by the parent's. |
243 | 274 | ||
275 | "path" may not be an absolute path or use "." or ".." path components. | ||
276 | These restrictions are not enforced for [Local Manifests]. | ||
277 | |||
278 | If you want to place files into the root of the checkout (e.g. a README or | ||
279 | Makefile or another build script), use the [copyfile] or [linkfile] elements | ||
280 | instead. | ||
281 | |||
244 | Attribute `remote`: Name of a previously defined remote element. | 282 | Attribute `remote`: Name of a previously defined remote element. |
245 | If not supplied the remote given by the default element is used. | 283 | If not supplied the remote given by the default element is used. |
246 | 284 | ||
247 | Attribute `revision`: Name of the Git branch the manifest wants | 285 | Attribute `revision`: Name of the Git branch the manifest wants |
248 | to track for this project. Names can be relative to refs/heads | 286 | to 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"). |
250 | Tags and/or explicit SHA-1s should work in theory, but have not | 288 | Tags and/or explicit SHA-1s should work in theory, but have not |
251 | been extensively tested. If not supplied the revision given by | 289 | been extensively tested. If not supplied the revision given by |
252 | the remote element is used if applicable, else the default | 290 | the remote element is used if applicable, else the default |
253 | element is used. | 291 | element is used. |
254 | 292 | ||
255 | Attribute `dest-branch`: Name of a Git branch (e.g. `master`). | 293 | Attribute `dest-branch`: Name of a Git branch (e.g. `main`). |
256 | When using `repo upload`, changes will be submitted for code | 294 | When using `repo upload`, changes will be submitted for code |
257 | review on this branch. If unspecified both here and in the | 295 | review on this branch. If unspecified both here and in the |
258 | default element, `revision` is used instead. | 296 | default element, `revision` is used instead. |
@@ -261,7 +299,7 @@ Attribute `groups`: List of groups to which this project belongs, | |||
261 | whitespace or comma separated. All projects belong to the group | 299 | whitespace 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 |
263 | its name:`name` and path:`path`. E.g. for | 301 | its 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 |
265 | definition is implicitly in the following manifest groups: | 303 | definition is implicitly in the following manifest groups: |
266 | default, name:monkeys, and path:barrel-of. If you place a project in the | 304 | default, name:monkeys, and path:barrel-of. If you place a project in the |
267 | group "notdefault", it will not be automatically downloaded by repo. | 305 | group "notdefault", it will not be automatically downloaded by repo. |
@@ -300,21 +338,29 @@ against changes to the original manifest. | |||
300 | Attribute `path`: If specified, limit the change to projects checked out | 338 | Attribute `path`: If specified, limit the change to projects checked out |
301 | at the specified path, rather than all projects with the given name. | 339 | at the specified path, rather than all projects with the given name. |
302 | 340 | ||
341 | Attribute `dest-path`: If specified, a path relative to the top directory | ||
342 | of the repo client where the Git working directory for this project | ||
343 | should be placed. This is used to move a project in the checkout by | ||
344 | overriding the existing `path` setting. | ||
345 | |||
303 | Attribute `groups`: List of additional groups to which this project | 346 | Attribute `groups`: List of additional groups to which this project |
304 | belongs. Same syntax as the corresponding element of `project`. | 347 | belongs. Same syntax as the corresponding element of `project`. |
305 | 348 | ||
306 | Attribute `revision`: If specified, overrides the revision of the original | 349 | Attribute `revision`: If specified, overrides the revision of the original |
307 | project. Same syntax as the corresponding element of `project`. | 350 | project. Same syntax as the corresponding element of `project`. |
308 | 351 | ||
352 | Attribute `remote`: If specified, overrides the remote of the original | ||
353 | project. Same syntax as the corresponding element of `project`. | ||
354 | |||
309 | ### Element annotation | 355 | ### Element annotation |
310 | 356 | ||
311 | Zero or more annotation elements may be specified as children of a | 357 | Zero or more annotation elements may be specified as children of a |
312 | project element. Each element describes a name-value pair that will be | 358 | project or remote element. Each element describes a name-value pair. |
313 | exported into each project's environment during a 'forall' command, | 359 | For projects, this name-value pair will be exported into each project's |
314 | prefixed with REPO__. In addition, there is an optional attribute | 360 | environment during a 'forall' command, prefixed with `REPO__`. In addition, |
315 | "keep" which accepts the case insensitive values "true" (default) or | 361 | there 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 |
317 | be kept when exported with the manifest subcommand. | 363 | annotation 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 | |||
338 | instead of copying it creates a symlink. | 384 | instead of copying it creates a symlink. |
339 | 385 | ||
340 | The symlink is created at "dest" (relative to the top of the tree) and | 386 | The symlink is created at "dest" (relative to the top of the tree) and |
341 | points to the path specified by "src". | 387 | points to the path specified by "src" which is a path in the project. |
342 | 388 | ||
343 | Parent directories of "dest" will be automatically created if missing. | 389 | Parent 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 | |||
355 | the user can remove a project, and possibly replace it with their | 401 | the user can remove a project, and possibly replace it with their |
356 | own definition. | 402 | own definition. |
357 | 403 | ||
404 | Attribute `optional`: Set to true to ignore remove-project elements with no | ||
405 | matching `project` element. | ||
406 | |||
407 | ### Element repo-hooks | ||
408 | |||
409 | NB: See the [practical documentation](./repo-hooks.md) for using repo hooks. | ||
410 | |||
411 | Only one repo-hooks element may be specified at a time. | ||
412 | Attempting to redefine it will fail to parse. | ||
413 | |||
414 | Attribute `in-project`: The project where the hooks are defined. The value | ||
415 | must match the `name` attribute (**not** the `path` attribute) of a previously | ||
416 | defined `project` element. | ||
417 | |||
418 | Attribute `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 | |||
426 | NB: See the [git superprojects documentation]( | ||
427 | https://en.wikibooks.org/wiki/Git/Submodules_and_Superprojects) for background | ||
428 | information. | ||
429 | |||
430 | This 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 | ||
432 | reasonable defaults. At most one superproject may be specified. | ||
433 | Attempting to redefine it will fail to parse. | ||
434 | |||
435 | Attribute `name`: A unique name for the superproject. This attribute has the | ||
436 | same meaning as project's name attribute. See the | ||
437 | [element project](#element-project) for more information. | ||
438 | |||
439 | Attribute `remote`: Name of a previously defined remote element. | ||
440 | If not supplied the remote given by the default element is used. | ||
441 | |||
442 | Attribute `revision`: Name of the Git branch the manifest wants | ||
443 | to track for this superproject. If not supplied the revision given | ||
444 | by the remote element is used if applicable, else the default | ||
445 | element is used. | ||
446 | |||
447 | ### Element contactinfo | ||
448 | |||
449 | *** | ||
450 | *Note*: This is currently a WIP. | ||
451 | *** | ||
452 | |||
453 | This element is used to let manifest authors self-register contact info. | ||
454 | It has "bugurl" as a required atrribute. This element can be repeated, | ||
455 | and any later entries will clobber earlier ones. This would allow manifest | ||
456 | authors who extend manifests to specify their own contact info. | ||
457 | |||
458 | Attribute `bugurl`: The URL to file a bug against the manifest owner. | ||
459 | |||
358 | ### Element include | 460 | ### Element include |
359 | 461 | ||
360 | This element provides the capability of including another manifest | 462 | This 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. | |||
364 | Attribute `name`: the manifest to include, specified relative to | 466 | Attribute `name`: the manifest to include, specified relative to |
365 | the manifest repository's root. | 467 | the manifest repository's root. |
366 | 468 | ||
469 | "name" may not be an absolute path or use "." or ".." path components. | ||
470 | These restrictions are not enforced for [Local Manifests]. | ||
471 | |||
472 | Attribute `groups`: List of additional groups to which all projects | ||
473 | in the included manifest belong. This appends and recurses, meaning | ||
474 | all projects in sub-manifests carry all parent include groups. | ||
475 | Same syntax as the corresponding element of `project`. | ||
367 | 476 | ||
368 | ## Local Manifests | 477 | ## Local Manifests {#local-manifests} |
369 | 478 | ||
370 | Additional remotes and projects may be added through local manifest | 479 | Additional remotes and projects may be added through local manifest |
371 | files stored in `$TOP_DIR/.repo/local_manifests/*.xml`. | 480 | files stored in `$TOP_DIR/.repo/local_manifests/*.xml`. |
@@ -392,10 +501,12 @@ these extra projects. | |||
392 | Manifest files stored in `$TOP_DIR/.repo/local_manifests/*.xml` will | 501 | Manifest files stored in `$TOP_DIR/.repo/local_manifests/*.xml` will |
393 | be loaded in alphabetical order. | 502 | be loaded in alphabetical order. |
394 | 503 | ||
395 | Additional remotes and projects may also be added through a local | 504 | Projects from local manifest files are added into |
396 | manifest, stored in `$TOP_DIR/.repo/local_manifest.xml`. This method | 505 | local::<local manifest filename> group. |
397 | is deprecated in favor of using multiple manifest files as mentioned | 506 | |
398 | above. | 507 | The legacy `$TOP_DIR/.repo/local_manifest.xml` path is no longer supported. |
508 | |||
399 | 509 | ||
400 | If `$TOP_DIR/.repo/local_manifest.xml` exists, it will be loaded before | 510 | [copyfile]: #Element-copyfile |
401 | any 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 | |||
18 | no new features will be added, nor is support guaranteed. | 18 | no new features will be added, nor is support guaranteed. |
19 | 19 | ||
20 | Users can select this during `repo init` time via the [repo launcher]. | 20 | Users can select this during `repo init` time via the [repo launcher]. |
21 | Otherwise the default branches (e.g. stable & master) will be used which will | 21 | Otherwise the default branches (e.g. stable & main) will be used which will |
22 | require Python 3. | 22 | require Python 3. |
23 | 23 | ||
24 | This means the [repo launcher] needs to support both Python 2 & Python 3, but | 24 | This means the [repo launcher] needs to support both Python 2 & Python 3, but |
25 | since it doesn't import any other repo code, this shouldn't be too problematic. | 25 | since it doesn't import any other repo code, this shouldn't be too problematic. |
26 | 26 | ||
27 | The master branch will require Python 3.6 at a minimum. | 27 | The main branch will require Python 3.6 at a minimum. |
28 | If the system has an older version of Python 3, then users will have to select | 28 | If the system has an older version of Python 3, then users will have to select |
29 | the legacy Python 2 branch instead. | 29 | the 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 | |||
10 | There is no specific schedule for when releases are made. | ||
11 | Usually it's more along the lines of "enough minor changes have been merged", | ||
12 | or "there's a known issue the maintainers know should get fixed". | ||
13 | If you find a fix has been merged for an issue important to you, but hasn't been | ||
14 | released after a week or so, feel free to [contact] us to request a new release. | ||
15 | |||
16 | ### Release Freezes {#freeze} | ||
17 | |||
18 | We try to observe a regular schedule for when **not** to release. | ||
19 | If something goes wrong, staff need to be active in order to respond quickly & | ||
20 | effectively. | ||
21 | We also don't want to disrupt non-Google organizations if possible. | ||
22 | |||
23 | We 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 | ||
10 | The main repo script serves as a standalone program and is often referred to as | 41 | The 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 | ||
55 | Whenever `repo sync` is run, repo will check to see if an update is available. | 86 | Whenever `repo sync` is run, repo will, once every 24 hours, see if an update |
56 | It fetches the latest repo-branch from the repo-url. | 87 | is available. |
88 | It fetches the latest repo-rev from the repo-url. | ||
57 | Then it verifies that the latest commit in the branch has a valid signed tag | 89 | Then it verifies that the latest commit in the branch has a valid signed tag |
58 | using `git tag -v` (which uses gpg). | 90 | using `git tag -v` (which uses gpg). |
59 | If the tag is valid, then repo will update its internal checkout to it. | 91 | If 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 | ||
65 | If that tag cannot be verified, it gives up and forces the user to resolve. | 97 | If that tag cannot be verified, it gives up and forces the user to resolve. |
66 | 98 | ||
99 | ### Force an update | ||
100 | |||
101 | The `repo selfupdate` command can be used to force an immediate update. | ||
102 | It is not subject to the 24 hour limitation. | ||
103 | |||
67 | ## Branch management | 104 | ## Branch management |
68 | 105 | ||
69 | All development happens on the `master` branch and should generally be stable. | 106 | All development happens on the `main` branch and should generally be stable. |
70 | 107 | ||
71 | Since the repo launcher defaults to tracking the `stable` branch, it is not | 108 | Since the repo launcher defaults to tracking the `stable` branch, it is not |
72 | normally updated until a new release is available. | 109 | normally 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` | |||
81 | branch will be updated from `v1.9.x` to `v1.10.x`. | 118 | branch will be updated from `v1.9.x` to `v1.10.x`. |
82 | 119 | ||
83 | We don't have parallel release branches/series. | 120 | We don't have parallel release branches/series. |
84 | Typically all tags are made against the `master` branch and then pushed to the | 121 | Typically 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. |
86 | Since repo doesn't typically see a lot of changes, this tends to be OK. | 123 | Since 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 | ||
90 | When you want to create a new release, you'll need to select a good version and | 127 | When you want to create a new release, you'll need to select a good version and |
91 | create a signed tag using a key registered in repo itself. | 128 | create a signed tag using a key registered in repo itself. |
92 | Typically we just tag the latest version of the `master` branch. | 129 | Typically we just tag the latest version of the `main` branch. |
93 | The tag could be pushed now, but it won't be used by clients normally (since the | 130 | The tag could be pushed now, but it won't be used by clients normally (since the |
94 | default `repo-branch` setting is `stable`). | 131 | default `repo-rev` setting is `stable`). |
95 | This would allow some early testing on systems who explicitly select `master`. | 132 | This 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 | |||
203 | Here's a table showing the relationship of major tools, their EOL dates, and | ||
204 | their status in Ubuntu & Debian. | ||
205 | Those distros tend to be good indicators of how long we need to support things. | ||
206 | |||
207 | Things in bold indicate stuff to take note of, but does not guarantee that we | ||
208 | still support them. | ||
209 | Things 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. | |||
27 | For the full syntax, see the [repo manifest format](./manifest-format.md). | 27 | For the full syntax, see the [repo manifest format](./manifest-format.md). |
28 | 28 | ||
29 | Here's a short example from | 29 | Here'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). |
31 | The `<project>` line checks out the repohooks git repo to the local | 31 | The `<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 |
33 | with the name `platform/tools/repohooks` for hooks to run during the | 33 | with 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. | |||
19 | We will never add code specific to older versions of Windows. | 19 | We will never add code specific to older versions of Windows. |
20 | It might work, but it most likely won't, so please don't bother asking. | 20 | It 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. | ||
26 | Please report any bugs and be sure to maintain backups! | ||
27 | *** | ||
28 | |||
29 | The Repo 2.4 release introduced support for [Git worktrees][git-worktree]. | ||
30 | You don't have to worry about or understand this particular feature, so don't | ||
31 | worry if this section of the Git manual is particularly impenetrable. | ||
32 | |||
33 | The salient point is that Git worktrees allow Repo to create repo client | ||
34 | checkouts that do not require symlinks at all under Windows. | ||
35 | This means users no longer need Administrator access to sync code. | ||
36 | |||
37 | Simply use `--worktree` when running `repo init` to opt in. | ||
38 | |||
39 | This 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 | ||
47 | Git worktrees (see the previous section for more info). | ||
48 | *** | ||
23 | 49 | ||
24 | Repo will use symlinks heavily internally. | 50 | Repo will use symlinks heavily internally. |
25 | On *NIX platforms, this isn't an issue, but Windows makes it a bit difficult. | 51 | On *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 | ||
65 | You should make sure to be running Python 3.6 or newer under Windows. | 91 | Python 3.6 or newer is required. |
66 | Python 2 might work, but due to already limited platform testing, you should | 92 | Python 2 is known to be broken when running under Windows. |
67 | only run newer Python versions. | ||
68 | See our [Python Support](./python-support.md) document for more details. | 93 | See our [Python Support](./python-support.md) document for more details. |
69 | 94 | ||
70 | You can grab the latest Windows installer here:<br> | 95 | You can grab the latest Windows installer here:<br> |
@@ -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 | ||
17 | from __future__ import print_function | ||
18 | import os | 15 | import os |
19 | import re | 16 | import re |
20 | import sys | 17 | import sys |
@@ -24,6 +21,7 @@ import tempfile | |||
24 | from error import EditorError | 21 | from error import EditorError |
25 | import platform_utils | 22 | import platform_utils |
26 | 23 | ||
24 | |||
27 | class Editor(object): | 25 | class 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. |
61 | Tried to fall back to vi but terminal is dumb. Please configure at | 59 | Tried to fall back to vi but terminal is dumb. Please configure at |
62 | least one of these before using this command.""", file=sys.stderr) | 60 | least 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') |
@@ -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 | |||
17 | class ManifestParseError(Exception): | 16 | class ManifestParseError(Exception): |
18 | """Failed to parse the manifest file. | 17 | """Failed to parse the manifest file. |
19 | """ | 18 | """ |
20 | 19 | ||
21 | class ManifestInvalidRevisionError(Exception): | 20 | |
21 | class 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 | |||
26 | class ManifestInvalidPathError(ManifestParseError): | ||
27 | """A path used in <copyfile> or <linkfile> is incorrect. | ||
28 | """ | ||
29 | |||
30 | |||
25 | class NoManifestException(Exception): | 31 | class 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 | |||
36 | class EditorError(Exception): | 44 | class 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 | |||
46 | class GitError(Exception): | 56 | class 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 | |||
56 | class UploadError(Exception): | 68 | class 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 | |||
66 | class DownloadError(Exception): | 80 | class 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 | |||
76 | class NoSuchProjectError(Exception): | 92 | class 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): | |||
89 | class InvalidProjectGroupsError(Exception): | 106 | class 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 | |||
101 | class RepoChangedException(Exception): | 120 | class 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 | |||
110 | class HookError(Exception): | 131 | class 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 | ||
17 | from __future__ import print_function | ||
18 | |||
19 | import json | 15 | import json |
20 | import multiprocessing | 16 | import multiprocessing |
21 | 17 | ||
@@ -23,6 +19,7 @@ TASK_COMMAND = 'command' | |||
23 | TASK_SYNC_NETWORK = 'sync-network' | 19 | TASK_SYNC_NETWORK = 'sync-network' |
24 | TASK_SYNC_LOCAL = 'sync-local' | 20 | TASK_SYNC_LOCAL = 'sync-local' |
25 | 21 | ||
22 | |||
26 | class EventLog(object): | 23 | class 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 | |||
168 | def _NextEventId(): | 166 | def _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 | |||
17 | import subprocess | ||
18 | import sys | ||
19 | from urllib.parse import urlparse | ||
20 | |||
21 | def 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 | ||
17 | from __future__ import print_function | 15 | import functools |
18 | import os | 16 | import os |
19 | import sys | 17 | import sys |
20 | import subprocess | 18 | import subprocess |
21 | import tempfile | ||
22 | from signal import SIGTERM | ||
23 | 19 | ||
24 | from error import GitError | 20 | from error import GitError |
25 | from git_refs import HEAD | 21 | from git_refs import HEAD |
@@ -28,75 +24,42 @@ from repo_trace import REPO_TRACE, IsTrace, Trace | |||
28 | from wrapper import Wrapper | 24 | from wrapper import Wrapper |
29 | 25 | ||
30 | GIT = 'git' | 26 | GIT = 'git' |
31 | MIN_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. | ||
36 | MIN_GIT_VERSION_SOFT = (1, 9, 1) | ||
37 | MIN_GIT_VERSION_HARD = (1, 7, 2) | ||
32 | GIT_DIR = 'GIT_DIR' | 38 | GIT_DIR = 'GIT_DIR' |
33 | 39 | ||
34 | LAST_GITDIR = None | 40 | LAST_GITDIR = None |
35 | LAST_CWD = None | 41 | LAST_CWD = None |
36 | 42 | ||
37 | _ssh_proxy_path = None | ||
38 | _ssh_sock_path = None | ||
39 | _ssh_clients = [] | ||
40 | |||
41 | def 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 | |||
54 | def _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 | |||
62 | def _add_ssh_client(p): | ||
63 | _ssh_clients.append(p) | ||
64 | |||
65 | def _remove_ssh_client(p): | ||
66 | try: | ||
67 | _ssh_clients.remove(p) | ||
68 | except ValueError: | ||
69 | pass | ||
70 | |||
71 | def 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 | ||
83 | class _GitCall(object): | 44 | class _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 | |||
100 | git = _GitCall() | 63 | git = _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 | |||
180 | user_agent = UserAgent() | 144 | user_agent = UserAgent() |
181 | 145 | ||
146 | |||
182 | def git_require(min_version, fail=False, msg=''): | 147 | def 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 | ||
194 | def _setenv(env, name, value): | ||
195 | env[name] = value.encode() | ||
196 | 159 | ||
197 | class GitCommand(object): | 160 | class 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 | ||
17 | from __future__ import print_function | ||
18 | |||
19 | import contextlib | 15 | import contextlib |
16 | import datetime | ||
20 | import errno | 17 | import errno |
18 | from http.client import HTTPException | ||
21 | import json | 19 | import json |
22 | import os | 20 | import os |
23 | import re | 21 | import re |
24 | import ssl | 22 | import ssl |
25 | import subprocess | 23 | import subprocess |
26 | import sys | 24 | import sys |
27 | try: | 25 | import urllib.error |
28 | import threading as _threading | 26 | import urllib.request |
29 | except ImportError: | 27 | |
30 | import dummy_threading as _threading | ||
31 | import time | ||
32 | |||
33 | from pyversion import is_python3 | ||
34 | if is_python3(): | ||
35 | import urllib.request | ||
36 | import urllib.error | ||
37 | else: | ||
38 | import urllib2 | ||
39 | import imp | ||
40 | urllib = imp.new_module('urllib') | ||
41 | urllib.request = urllib2 | ||
42 | urllib.error = urllib2 | ||
43 | |||
44 | from signal import SIGTERM | ||
45 | from error import GitError, UploadError | 28 | from error import GitError, UploadError |
46 | import platform_utils | 29 | import platform_utils |
47 | from repo_trace import Trace | 30 | from repo_trace import Trace |
48 | if is_python3(): | ||
49 | from http.client import HTTPException | ||
50 | else: | ||
51 | from httplib import HTTPException | ||
52 | |||
53 | from git_command import GitCommand | 31 | from git_command import GitCommand |
54 | from git_command import ssh_sock | ||
55 | from git_command import terminate_ssh_clients | ||
56 | from git_refs import R_CHANGES, R_HEADS, R_TAGS | 32 | from 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. | ||
36 | SYNC_STATE_PREFIX = 'repo.syncstate.' | ||
37 | |||
58 | ID_RE = re.compile(r'^[0-9a-f]{40}$') | 38 | ID_RE = re.compile(r'^[0-9a-f]{40}$') |
59 | 39 | ||
60 | REVIEW_CACHE = dict() | 40 | REVIEW_CACHE = dict() |
61 | 41 | ||
42 | |||
62 | def IsChange(rev): | 43 | def IsChange(rev): |
63 | return rev.startswith(R_CHANGES) | 44 | return rev.startswith(R_CHANGES) |
64 | 45 | ||
46 | |||
65 | def IsId(rev): | 47 | def IsId(rev): |
66 | return ID_RE.match(rev) | 48 | return ID_RE.match(rev) |
67 | 49 | ||
50 | |||
68 | def IsTag(rev): | 51 | def IsTag(rev): |
69 | return rev.startswith(R_TAGS) | 52 | return rev.startswith(R_TAGS) |
70 | 53 | ||
54 | |||
71 | def IsImmutable(rev): | 55 | def 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 | |||
74 | def _key(name): | 59 | def _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 | |||
82 | class GitConfig(object): | 68 | class 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 | |||
409 | class RepoConfig(GitConfig): | ||
410 | """User settings for repo itself.""" | ||
411 | |||
412 | _USER_CONFIG = '~/.repoconfig/config' | ||
332 | 413 | ||
333 | 414 | ||
334 | class RefSpec(object): | 415 | class 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 | |||
395 | def 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 | |||
404 | def _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 | |||
484 | def 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 | |||
508 | URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):') | ||
509 | URI_ALL = re.compile(r'^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/') | 471 | URI_ALL = re.compile(r'^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/') |
510 | 472 | ||
473 | |||
511 | def GetSchemeFromUrl(url): | 474 | def 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 |
518 | def GetUrlCookieFile(url, quiet): | 482 | def 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 | ||
557 | def _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 | ||
577 | class Remote(object): | 522 | class 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 | ||
737 | class Branch(object): | 697 | class 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 | |||
747 | class 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 | |||
18 | from repo_trace import Trace | 16 | from repo_trace import Trace |
19 | import platform_utils | 17 | import platform_utils |
20 | 18 | ||
21 | HEAD = 'HEAD' | 19 | HEAD = 'HEAD' |
22 | R_CHANGES = 'refs/changes/' | 20 | R_CHANGES = 'refs/changes/' |
23 | R_HEADS = 'refs/heads/' | 21 | R_HEADS = 'refs/heads/' |
24 | R_TAGS = 'refs/tags/' | 22 | R_TAGS = 'refs/tags/' |
25 | R_PUB = 'refs/published/' | 23 | R_PUB = 'refs/published/' |
26 | R_M = 'refs/remotes/m/' | 24 | R_WORKTREE = 'refs/worktree/' |
25 | R_WORKTREE_M = R_WORKTREE + 'm/' | ||
26 | R_M = 'refs/remotes/m/' | ||
27 | 27 | ||
28 | 28 | ||
29 | class GitRefs(object): | 29 | class 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 | |||
17 | For more information on superproject, check out: | ||
18 | https://en.wikibooks.org/wiki/Git/Submodules_and_Superprojects | ||
19 | |||
20 | Examples: | ||
21 | superproject = Superproject() | ||
22 | UpdateProjectsResult = superproject.UpdateProjectsRevisionId(projects) | ||
23 | """ | ||
24 | |||
25 | import hashlib | ||
26 | import functools | ||
27 | import os | ||
28 | import sys | ||
29 | import time | ||
30 | from typing import NamedTuple | ||
31 | |||
32 | from git_command import git_require, GitCommand | ||
33 | from git_config import RepoConfig | ||
34 | from git_refs import R_HEADS | ||
35 | from manifest_xml import LOCAL_MANIFEST_GROUP_PREFIX | ||
36 | |||
37 | _SUPERPROJECT_GIT_NAME = 'superproject.git' | ||
38 | _SUPERPROJECT_MANIFEST_NAME = 'superproject_override.xml' | ||
39 | |||
40 | |||
41 | class 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 | |||
50 | class 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 | |||
59 | class 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 | |||
68 | class 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) | ||
355 | def _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 | |||
398 | def 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 | |||
403 | def 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 | |||
17 | The git trace2 EVENT format is defined at: | ||
18 | https://www.kernel.org/pub/software/scm/git/docs/technical/api-trace2.html#_event_format | ||
19 | https://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 | |||
31 | import datetime | ||
32 | import json | ||
33 | import os | ||
34 | import sys | ||
35 | import tempfile | ||
36 | import threading | ||
37 | |||
38 | from git_command import GitCommand, RepoSourceVersion | ||
39 | |||
40 | |||
41 | class 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 | ||
17 | from __future__ import print_function | ||
18 | import os | 15 | import os |
16 | import multiprocessing | ||
19 | import platform | 17 | import platform |
20 | import re | 18 | import re |
21 | import sys | 19 | import sys |
@@ -29,12 +27,24 @@ from error import ManifestParseError | |||
29 | 27 | ||
30 | NUM_BATCH_RETRIEVE_REVISIONID = 32 | 28 | NUM_BATCH_RETRIEVE_REVISIONID = 32 |
31 | 29 | ||
30 | |||
32 | def get_gitc_manifest_dir(): | 31 | def get_gitc_manifest_dir(): |
33 | return wrapper.Wrapper().get_gitc_manifest_dir() | 32 | return wrapper.Wrapper().get_gitc_manifest_dir() |
34 | 33 | ||
34 | |||
35 | def parse_clientdir(gitc_fs_path): | 35 | def 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 | |||
39 | def _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 | |||
38 | def _set_project_revisions(projects): | 48 | def _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)) | |
66 | def _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 | ||
80 | def generate_gitc_manifest(gitc_manifest, manifest, paths=None): | 80 | def 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 | |||
143 | def save_manifest(manifest, client_dir=None): | 141 | def 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 | |||
15 | import errno | ||
16 | import json | ||
17 | import os | ||
18 | import re | ||
19 | import subprocess | ||
20 | import sys | ||
21 | import traceback | ||
22 | import urllib.parse | ||
23 | |||
24 | from error import HookError | ||
25 | from git_refs import HEAD | ||
26 | |||
27 | |||
28 | class 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 = """ | ||
294 | import json, os, sys | ||
295 | path = '''%(path)s''' | ||
296 | kwargs = json.loads('''%(kwargs)s''') | ||
297 | context = json.loads('''%(context)s''') | ||
298 | sys.path.insert(0, os.path.dirname(path)) | ||
299 | data = open(path).read() | ||
300 | exec(compile(data, path, 'exec'), context) | ||
301 | context['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 | |||
21 | unset GREP_OPTIONS | ||
22 | |||
23 | CHANGE_ID_AFTER="Bug|Depends-On|Issue|Test|Feature|Fixes|Fixed" | ||
24 | MSG="$1" | ||
25 | |||
26 | # Check for, and add if missing, a unique Change-Id | ||
27 | # | ||
28 | add_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 | ||
191 | add_ChangeId | 20 | # avoid [[ which is not POSIX sh. |
21 | if test "$#" != 1 ; then | ||
22 | echo "$0 requires an argument." | ||
23 | exit 1 | ||
24 | fi | ||
25 | |||
26 | if test ! -f "$1" ; then | ||
27 | echo "file does not exist: $1" | ||
28 | exit 1 | ||
29 | fi | ||
30 | |||
31 | # Do not create a change id if requested | ||
32 | if test "false" = "`git config --bool --get gerrit.createChangeId`" ; then | ||
33 | exit 0 | ||
34 | fi | ||
35 | |||
36 | # $RANDOM will be undefined if not using bash, so don't use set -u | ||
37 | random=$( (whoami ; hostname ; date; cat $1 ; echo $RANDOM) | git hash-object --stdin) | ||
38 | dest="$1.tmp.${random}" | ||
39 | |||
40 | trap 'rm -f "${dest}"' EXIT | ||
41 | |||
42 | if ! git stripspace --strip-comments < "$1" > "${dest}" ; then | ||
43 | echo "cannot strip comments from $1" | ||
44 | exit 1 | ||
45 | fi | ||
46 | |||
47 | if test ! -s "${dest}" ; then | ||
48 | echo "file is empty: $1" | ||
49 | exit 1 | ||
50 | fi | ||
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 | ||
54 | if ! 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 | ||
58 | fi | ||
59 | |||
60 | if ! mv "${dest}" "$1" ; then | ||
61 | echo "cannot mv ${dest} to $1" | ||
62 | exit 1 | ||
63 | fi | ||
@@ -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 | |||
21 | which takes care of execing this entry point. | 20 | which takes care of execing this entry point. |
22 | """ | 21 | """ |
23 | 22 | ||
24 | from __future__ import print_function | ||
25 | import getpass | 23 | import getpass |
26 | import netrc | 24 | import netrc |
27 | import optparse | 25 | import optparse |
28 | import os | 26 | import os |
27 | import shlex | ||
29 | import sys | 28 | import sys |
30 | import textwrap | 29 | import textwrap |
31 | import time | 30 | import time |
32 | 31 | import urllib.request | |
33 | from pyversion import is_python3 | ||
34 | if is_python3(): | ||
35 | import urllib.request | ||
36 | else: | ||
37 | import imp | ||
38 | import urllib2 | ||
39 | urllib = imp.new_module('urllib') | ||
40 | urllib.request = urllib2 | ||
41 | 32 | ||
42 | try: | 33 | try: |
43 | import kerberos | 34 | import kerberos |
@@ -47,8 +38,9 @@ except ImportError: | |||
47 | from color import SetDefaultColoring | 38 | from color import SetDefaultColoring |
48 | import event_log | 39 | import event_log |
49 | from repo_trace import SetTrace | 40 | from repo_trace import SetTrace |
50 | from git_command import git, GitCommand, user_agent | 41 | from git_command import user_agent |
51 | from git_config import init_ssh, close_ssh | 42 | from git_config import RepoConfig |
43 | from git_trace2_event_log import EventLog | ||
52 | from command import InteractiveCommand | 44 | from command import InteractiveCommand |
53 | from command import MirrorSafeCommand | 45 | from command import MirrorSafeCommand |
54 | from command import GitcAvailableCommand, GitcClientCommand | 46 | from command import GitcAvailableCommand, GitcClientCommand |
@@ -62,25 +54,54 @@ from error import NoManifestException | |||
62 | from error import NoSuchProjectError | 54 | from error import NoSuchProjectError |
63 | from error import RepoChangedException | 55 | from error import RepoChangedException |
64 | import gitc_utils | 56 | import gitc_utils |
65 | from manifest_xml import GitcManifest, XmlManifest | 57 | from manifest_xml import GitcClient, RepoClient |
66 | from pager import RunPager, TerminatePager | 58 | from pager import RunPager, TerminatePager |
67 | from wrapper import WrapperPath, Wrapper | 59 | from wrapper import WrapperPath, Wrapper |
68 | 60 | ||
69 | from subcmds import all_commands | 61 | from subcmds import all_commands |
70 | 62 | ||
71 | if 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. | ||
73 | MIN_PYTHON_VERSION_SOFT = (3, 6) | ||
74 | MIN_PYTHON_VERSION_HARD = (3, 6) | ||
75 | |||
76 | if 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) | ||
81 | else: | ||
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 | ||
74 | global_options = optparse.OptionParser( | 93 | global_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) |
77 | global_options.add_option('-h', '--help', action='store_true', | 96 | global_options.add_option('-h', '--help', action='store_true', |
78 | help='show this help message and exit') | 97 | help='show this help message and exit') |
98 | global_options.add_option('--help-all', action='store_true', | ||
99 | help='show this help message with all subcommands and exit') | ||
79 | global_options.add_option('-p', '--paginate', | 100 | global_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') |
82 | global_options.add_option('--no-pager', | 103 | global_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') |
85 | global_options.add_option('--color', | 106 | global_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', | |||
97 | global_options.add_option('--version', | 118 | global_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') |
121 | global_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') | ||
100 | global_options.add_option('--event-log', | 125 | global_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') |
128 | global_options.add_option('--git-trace2-event-log', action='store', | ||
129 | help='directory to write git trace2 event log to') | ||
130 | |||
103 | 131 | ||
104 | class _Repo(object): | 132 | class _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 | ||
258 | def _CheckWrapperVersion(ver, repo_path): | 338 | def _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. !!! | 365 | repo: 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 | ||
289 | def _CheckRepoDir(repo_dir): | 392 | def _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 | |||
294 | def _PruneOptions(argv, opt): | 398 | def _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 | |||
309 | class _UserAgentHandler(urllib.request.BaseHandler): | 414 | class _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 | |||
318 | def _AddPasswordFromUserInput(handler, msg, req): | 424 | def _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 | |||
331 | class _BasicAuthHandler(urllib.request.HTTPBasicAuthHandler): | 438 | class _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 | |||
354 | class _DigestAuthHandler(urllib.request.HTTPDigestAuthHandler): | 463 | class _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 | |||
377 | class _KerberosAuthHandler(urllib.request.BaseHandler): | 488 | class _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 | |||
457 | def init_http(): | 569 | def 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 | |||
484 | def _Main(argv): | 597 | def _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 | |||
540 | if __name__ == '__main__': | 650 | if __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 | ||
4 | repo \- 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 | ||
9 | Summary | ||
10 | .PP | ||
11 | Permanently abandon a development branch | ||
12 | .PP | ||
13 | This subcommand permanently abandons a development branch by | ||
14 | deleting it (and all its history) from your local repository. | ||
15 | .PP | ||
16 | It is equivalent to "git branch \fB\-D\fR <branchname>". | ||
17 | .SH OPTIONS | ||
18 | .TP | ||
19 | \fB\-h\fR, \fB\-\-help\fR | ||
20 | show this help message and exit | ||
21 | .TP | ||
22 | \fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR | ||
23 | number of jobs to run in parallel (default: based on | ||
24 | number of CPU cores) | ||
25 | .TP | ||
26 | \fB\-\-all\fR | ||
27 | delete all branches in all projects | ||
28 | .SS Logging options: | ||
29 | .TP | ||
30 | \fB\-v\fR, \fB\-\-verbose\fR | ||
31 | show all output | ||
32 | .TP | ||
33 | \fB\-q\fR, \fB\-\-quiet\fR | ||
34 | only show errors | ||
35 | .PP | ||
36 | Run `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 | ||
4 | repo \- repo branches - manual page for repo branches | ||
5 | .SH SYNOPSIS | ||
6 | .B repo | ||
7 | \fI\,branches \/\fR[\fI\,<project>\/\fR...] | ||
8 | .SH DESCRIPTION | ||
9 | Summary | ||
10 | .PP | ||
11 | View current topic branches | ||
12 | .PP | ||
13 | Summarizes the currently available topic branches. | ||
14 | .PP | ||
15 | # Branch Display | ||
16 | .PP | ||
17 | The branch display output by this command is organized into four | ||
18 | columns of information; for example: | ||
19 | .TP | ||
20 | *P nocolor | ||
21 | | in repo | ||
22 | .TP | ||
23 | repo2 | ||
24 | | | ||
25 | .PP | ||
26 | The first column contains a * if the branch is the currently | ||
27 | checked out branch in any of the specified projects, or a blank | ||
28 | if no project has the branch checked out. | ||
29 | .PP | ||
30 | The second column contains either blank, p or P, depending upon | ||
31 | the upload status of the branch. | ||
32 | .IP | ||
33 | (blank): branch not yet published by repo upload | ||
34 | .IP | ||
35 | P: all commits were published by repo upload | ||
36 | p: only some commits were published by repo upload | ||
37 | .PP | ||
38 | The third column contains the branch name. | ||
39 | .PP | ||
40 | The fourth column (after the | separator) lists the projects that | ||
41 | the branch appears in, or does not appear in. If no project list | ||
42 | is shown, then the branch appears in all projects. | ||
43 | .SH OPTIONS | ||
44 | .TP | ||
45 | \fB\-h\fR, \fB\-\-help\fR | ||
46 | show this help message and exit | ||
47 | .TP | ||
48 | \fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR | ||
49 | number of jobs to run in parallel (default: based on | ||
50 | number of CPU cores) | ||
51 | .SS Logging options: | ||
52 | .TP | ||
53 | \fB\-v\fR, \fB\-\-verbose\fR | ||
54 | show all output | ||
55 | .TP | ||
56 | \fB\-q\fR, \fB\-\-quiet\fR | ||
57 | only show errors | ||
58 | .PP | ||
59 | Run `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 | ||
4 | repo \- repo checkout - manual page for repo checkout | ||
5 | .SH SYNOPSIS | ||
6 | .B repo | ||
7 | \fI\,checkout <branchname> \/\fR[\fI\,<project>\/\fR...] | ||
8 | .SH DESCRIPTION | ||
9 | Summary | ||
10 | .PP | ||
11 | Checkout a branch for development | ||
12 | .SH OPTIONS | ||
13 | .TP | ||
14 | \fB\-h\fR, \fB\-\-help\fR | ||
15 | show this help message and exit | ||
16 | .TP | ||
17 | \fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR | ||
18 | number of jobs to run in parallel (default: based on | ||
19 | number of CPU cores) | ||
20 | .SS Logging options: | ||
21 | .TP | ||
22 | \fB\-v\fR, \fB\-\-verbose\fR | ||
23 | show all output | ||
24 | .TP | ||
25 | \fB\-q\fR, \fB\-\-quiet\fR | ||
26 | only show errors | ||
27 | .PP | ||
28 | Run `repo help checkout` to view the detailed manual. | ||
29 | .SH DETAILS | ||
30 | .PP | ||
31 | The 'repo checkout' command checks out an existing branch that was previously | ||
32 | created by 'repo start'. | ||
33 | .PP | ||
34 | The command is equivalent to: | ||
35 | .IP | ||
36 | repo 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 | ||
4 | repo \- repo cherry-pick - manual page for repo cherry-pick | ||
5 | .SH SYNOPSIS | ||
6 | .B repo | ||
7 | \fI\,cherry-pick <sha1>\/\fR | ||
8 | .SH DESCRIPTION | ||
9 | Summary | ||
10 | .PP | ||
11 | Cherry\-pick a change. | ||
12 | .SH OPTIONS | ||
13 | .TP | ||
14 | \fB\-h\fR, \fB\-\-help\fR | ||
15 | show this help message and exit | ||
16 | .SS Logging options: | ||
17 | .TP | ||
18 | \fB\-v\fR, \fB\-\-verbose\fR | ||
19 | show all output | ||
20 | .TP | ||
21 | \fB\-q\fR, \fB\-\-quiet\fR | ||
22 | only show errors | ||
23 | .PP | ||
24 | Run `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 | ||
28 | id 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 | ||
4 | repo \- repo diff - manual page for repo diff | ||
5 | .SH SYNOPSIS | ||
6 | .B repo | ||
7 | \fI\,diff \/\fR[\fI\,<project>\/\fR...] | ||
8 | .SH DESCRIPTION | ||
9 | Summary | ||
10 | .PP | ||
11 | Show changes between commit and working tree | ||
12 | .PP | ||
13 | The \fB\-u\fR option causes 'repo diff' to generate diff output with file paths | ||
14 | relative to the repository root, so the output can be applied | ||
15 | to the Unix 'patch' command. | ||
16 | .SH OPTIONS | ||
17 | .TP | ||
18 | \fB\-h\fR, \fB\-\-help\fR | ||
19 | show this help message and exit | ||
20 | .TP | ||
21 | \fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR | ||
22 | number of jobs to run in parallel (default: based on | ||
23 | number of CPU cores) | ||
24 | .TP | ||
25 | \fB\-u\fR, \fB\-\-absolute\fR | ||
26 | paths are relative to the repository root | ||
27 | .SS Logging options: | ||
28 | .TP | ||
29 | \fB\-v\fR, \fB\-\-verbose\fR | ||
30 | show all output | ||
31 | .TP | ||
32 | \fB\-q\fR, \fB\-\-quiet\fR | ||
33 | only show errors | ||
34 | .PP | ||
35 | Run `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 | ||
4 | repo \- repo diffmanifests - manual page for repo diffmanifests | ||
5 | .SH SYNOPSIS | ||
6 | .B repo | ||
7 | \fI\,diffmanifests manifest1.xml \/\fR[\fI\,manifest2.xml\/\fR] [\fI\,options\/\fR] | ||
8 | .SH DESCRIPTION | ||
9 | Summary | ||
10 | .PP | ||
11 | Manifest diff utility | ||
12 | .SH OPTIONS | ||
13 | .TP | ||
14 | \fB\-h\fR, \fB\-\-help\fR | ||
15 | show this help message and exit | ||
16 | .TP | ||
17 | \fB\-\-raw\fR | ||
18 | display raw diff | ||
19 | .TP | ||
20 | \fB\-\-no\-color\fR | ||
21 | does not display the diff in color | ||
22 | .TP | ||
23 | \fB\-\-pretty\-format=\fR<FORMAT> | ||
24 | print the log using a custom git pretty format string | ||
25 | .SS Logging options: | ||
26 | .TP | ||
27 | \fB\-v\fR, \fB\-\-verbose\fR | ||
28 | show all output | ||
29 | .TP | ||
30 | \fB\-q\fR, \fB\-\-quiet\fR | ||
31 | only show errors | ||
32 | .PP | ||
33 | Run `repo help diffmanifests` to view the detailed manual. | ||
34 | .SH DETAILS | ||
35 | .PP | ||
36 | The repo diffmanifests command shows differences between project revisions of | ||
37 | manifest1 and manifest2. if manifest2 is not specified, current manifest.xml | ||
38 | will be used instead. Both absolute and relative paths may be used for | ||
39 | manifests. Relative paths start from project's ".repo/manifests" folder. | ||
40 | .PP | ||
41 | The \fB\-\-raw\fR option Displays the diff in a way that facilitates parsing, the | ||
42 | project pattern will be <status> <path> <revision from> [<revision to>] and the | ||
43 | commit pattern will be <status> <onelined log> with status values respectively : | ||
44 | .IP | ||
45 | A = Added project | ||
46 | R = Removed project | ||
47 | C = Changed project | ||
48 | U = Project with unreachable revision(s) (revision(s) not found) | ||
49 | .PP | ||
50 | for project, and | ||
51 | .IP | ||
52 | A = Added commit | ||
53 | R = Removed commit | ||
54 | .PP | ||
55 | for a commit. | ||
56 | .PP | ||
57 | Only changed projects may contain commits, and commit status always starts with | ||
58 | a space, and are part of last printed project. Unreachable revisions may occur | ||
59 | if project is not up to date or if repo has not been initialized with all the | ||
60 | groups, in which case some projects won't be synced and their revisions won't be | ||
61 | found. | ||
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 | ||
4 | repo \- repo download - manual page for repo download | ||
5 | .SH SYNOPSIS | ||
6 | .B repo | ||
7 | \fI\,download {\/\fR[\fI\,project\/\fR] \fI\,change\/\fR[\fI\,/patchset\/\fR]\fI\,}\/\fR... | ||
8 | .SH DESCRIPTION | ||
9 | Summary | ||
10 | .PP | ||
11 | Download and checkout a change | ||
12 | .SH OPTIONS | ||
13 | .TP | ||
14 | \fB\-h\fR, \fB\-\-help\fR | ||
15 | show this help message and exit | ||
16 | .TP | ||
17 | \fB\-b\fR BRANCH, \fB\-\-branch\fR=\fI\,BRANCH\/\fR | ||
18 | create a new branch first | ||
19 | .TP | ||
20 | \fB\-c\fR, \fB\-\-cherry\-pick\fR | ||
21 | cherry\-pick instead of checkout | ||
22 | .TP | ||
23 | \fB\-x\fR, \fB\-\-record\-origin\fR | ||
24 | pass \fB\-x\fR when cherry\-picking | ||
25 | .TP | ||
26 | \fB\-r\fR, \fB\-\-revert\fR | ||
27 | revert instead of checkout | ||
28 | .TP | ||
29 | \fB\-f\fR, \fB\-\-ff\-only\fR | ||
30 | force fast\-forward merge | ||
31 | .SS Logging options: | ||
32 | .TP | ||
33 | \fB\-v\fR, \fB\-\-verbose\fR | ||
34 | show all output | ||
35 | .TP | ||
36 | \fB\-q\fR, \fB\-\-quiet\fR | ||
37 | only show errors | ||
38 | .PP | ||
39 | Run `repo help download` to view the detailed manual. | ||
40 | .SH DETAILS | ||
41 | .PP | ||
42 | The 'repo download' command downloads a change from the review system and makes | ||
43 | it available in your project's local working directory. If no project is | ||
44 | specified 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 | ||
4 | repo \- 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 | ||
9 | Summary | ||
10 | .PP | ||
11 | Run a shell command in each project | ||
12 | .PP | ||
13 | repo forall \fB\-r\fR str1 [str2] ... \fB\-c\fR <command> [<arg>...] | ||
14 | .SH OPTIONS | ||
15 | .TP | ||
16 | \fB\-h\fR, \fB\-\-help\fR | ||
17 | show this help message and exit | ||
18 | .TP | ||
19 | \fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR | ||
20 | number of jobs to run in parallel (default: based on | ||
21 | number of CPU cores) | ||
22 | .TP | ||
23 | \fB\-r\fR, \fB\-\-regex\fR | ||
24 | execute the command only on projects matching regex or | ||
25 | wildcard expression | ||
26 | .TP | ||
27 | \fB\-i\fR, \fB\-\-inverse\-regex\fR | ||
28 | execute the command only on projects not matching | ||
29 | regex or wildcard expression | ||
30 | .TP | ||
31 | \fB\-g\fR GROUPS, \fB\-\-groups\fR=\fI\,GROUPS\/\fR | ||
32 | execute the command only on projects matching the | ||
33 | specified groups | ||
34 | .TP | ||
35 | \fB\-c\fR, \fB\-\-command\fR | ||
36 | command (and arguments) to execute | ||
37 | .TP | ||
38 | \fB\-e\fR, \fB\-\-abort\-on\-errors\fR | ||
39 | abort if a command exits unsuccessfully | ||
40 | .TP | ||
41 | \fB\-\-ignore\-missing\fR | ||
42 | silently skip & do not exit non\-zero due missing | ||
43 | checkouts | ||
44 | .TP | ||
45 | \fB\-\-interactive\fR | ||
46 | force interactive usage | ||
47 | .SS Logging options: | ||
48 | .TP | ||
49 | \fB\-v\fR, \fB\-\-verbose\fR | ||
50 | show all output | ||
51 | .TP | ||
52 | \fB\-q\fR, \fB\-\-quiet\fR | ||
53 | only show errors | ||
54 | .TP | ||
55 | \fB\-p\fR | ||
56 | show project headers before output | ||
57 | .PP | ||
58 | Run `repo help forall` to view the detailed manual. | ||
59 | .SH DETAILS | ||
60 | .PP | ||
61 | Executes the same shell command in each project. | ||
62 | .PP | ||
63 | The \fB\-r\fR option allows running the command only on projects matching regex or | ||
64 | wildcard expression. | ||
65 | .PP | ||
66 | By default, projects are processed non\-interactively in parallel. If you want to | ||
67 | run interactive commands, make sure to pass \fB\-\-interactive\fR to force \fB\-\-jobs\fR 1. | ||
68 | While the processing order of projects is not guaranteed, the order of project | ||
69 | output is stable. | ||
70 | .PP | ||
71 | Output Formatting | ||
72 | .PP | ||
73 | The \fB\-p\fR option causes 'repo forall' to bind pipes to the command's stdin, stdout | ||
74 | and stderr streams, and pipe all output into a continuous stream that is | ||
75 | displayed in a single pager session. Project headings are inserted before the | ||
76 | output of each command is displayed. If the command produces no output in a | ||
77 | project, no heading is displayed. | ||
78 | .PP | ||
79 | The formatting convention used by \fB\-p\fR is very suitable for some types of | ||
80 | searching, e.g. `repo forall \fB\-p\fR \fB\-c\fR git log \fB\-SFoo\fR` will print all commits that | ||
81 | add or remove references to Foo. | ||
82 | .PP | ||
83 | The \fB\-v\fR option causes 'repo forall' to display stderr messages if a command | ||
84 | produces output only on stderr. Normally the \fB\-p\fR option causes command output to | ||
85 | be suppressed until the command produces at least one byte of output on stdout. | ||
86 | .PP | ||
87 | Environment | ||
88 | .PP | ||
89 | pwd is the project's working directory. If the current client is a mirror | ||
90 | client, then pwd is the Git repository. | ||
91 | .PP | ||
92 | REPO_PROJECT is set to the unique name of the project. | ||
93 | .PP | ||
94 | REPO_PATH is the path relative the the root of the client. | ||
95 | .PP | ||
96 | REPO_REMOTE is the name of the remote system from the manifest. | ||
97 | .PP | ||
98 | REPO_LREV is the name of the revision from the manifest, translated to a local | ||
99 | tracking branch. If you need to pass the manifest revision to a locally executed | ||
100 | git command, use REPO_LREV. | ||
101 | .PP | ||
102 | REPO_RREV is the name of the revision from the manifest, exactly as written in | ||
103 | the manifest. | ||
104 | .PP | ||
105 | REPO_COUNT is the total number of projects being iterated. | ||
106 | .PP | ||
107 | REPO_I is the current (1\-based) iteration count. Can be used in conjunction with | ||
108 | REPO_COUNT to add a simple progress indicator to your command. | ||
109 | .PP | ||
110 | REPO__* are any extra environment variables, specified by the "annotation" | ||
111 | element under any project element. This can be useful for differentiating trees | ||
112 | based on user\-specific criteria, or simply annotating tree details. | ||
113 | .PP | ||
114 | shell positional arguments ($1, $2, .., $#) are set to any arguments following | ||
115 | <command>. | ||
116 | .PP | ||
117 | Example: to list projects: | ||
118 | .IP | ||
119 | repo forall \fB\-c\fR 'echo $REPO_PROJECT' | ||
120 | .PP | ||
121 | Notice that $REPO_PROJECT is quoted to ensure it is expanded in the context of | ||
122 | running <command> instead of in the calling shell. | ||
123 | .PP | ||
124 | Unless \fB\-p\fR is used, stdin, stdout, stderr are inherited from the terminal and are | ||
125 | not redirected. | ||
126 | .PP | ||
127 | If \fB\-e\fR is used, when a command exits unsuccessfully, 'repo forall' will abort | ||
128 | without 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 | ||
4 | repo \- repo gitc-delete - manual page for repo gitc-delete | ||
5 | .SH SYNOPSIS | ||
6 | .B repo | ||
7 | \fI\,gitc-delete\/\fR | ||
8 | .SH DESCRIPTION | ||
9 | Summary | ||
10 | .PP | ||
11 | Delete a GITC Client. | ||
12 | .SH OPTIONS | ||
13 | .TP | ||
14 | \fB\-h\fR, \fB\-\-help\fR | ||
15 | show this help message and exit | ||
16 | .TP | ||
17 | \fB\-f\fR, \fB\-\-force\fR | ||
18 | force the deletion (no prompt) | ||
19 | .SS Logging options: | ||
20 | .TP | ||
21 | \fB\-v\fR, \fB\-\-verbose\fR | ||
22 | show all output | ||
23 | .TP | ||
24 | \fB\-q\fR, \fB\-\-quiet\fR | ||
25 | only show errors | ||
26 | .PP | ||
27 | Run `repo help gitc\-delete` to view the detailed manual. | ||
28 | .SH DETAILS | ||
29 | .PP | ||
30 | This subcommand deletes the current GITC client, deleting the GITC manifest and | ||
31 | all 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 | ||
4 | repo \- 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 | ||
9 | Summary | ||
10 | .PP | ||
11 | Initialize a GITC Client. | ||
12 | .SH OPTIONS | ||
13 | .TP | ||
14 | \fB\-h\fR, \fB\-\-help\fR | ||
15 | show this help message and exit | ||
16 | .SS Logging options: | ||
17 | .TP | ||
18 | \fB\-v\fR, \fB\-\-verbose\fR | ||
19 | show all output | ||
20 | .TP | ||
21 | \fB\-q\fR, \fB\-\-quiet\fR | ||
22 | only show errors | ||
23 | .SS Manifest options: | ||
24 | .TP | ||
25 | \fB\-u\fR URL, \fB\-\-manifest\-url\fR=\fI\,URL\/\fR | ||
26 | manifest repository location | ||
27 | .TP | ||
28 | \fB\-b\fR REVISION, \fB\-\-manifest\-branch\fR=\fI\,REVISION\/\fR | ||
29 | manifest branch or revision (use HEAD for default) | ||
30 | .TP | ||
31 | \fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml | ||
32 | initial manifest file | ||
33 | .TP | ||
34 | \fB\-\-standalone\-manifest\fR | ||
35 | download the manifest as a static file rather then | ||
36 | create a git checkout of the manifest repo | ||
37 | .TP | ||
38 | \fB\-g\fR GROUP, \fB\-\-groups\fR=\fI\,GROUP\/\fR | ||
39 | restrict manifest projects to ones with specified | ||
40 | group(s) [default|all|G1,G2,G3|G4,\-G5,\-G6] | ||
41 | .TP | ||
42 | \fB\-p\fR PLATFORM, \fB\-\-platform\fR=\fI\,PLATFORM\/\fR | ||
43 | restrict manifest projects to ones with a specified | ||
44 | platform group [auto|all|none|linux|darwin|...] | ||
45 | .TP | ||
46 | \fB\-\-submodules\fR | ||
47 | sync any submodules associated with the manifest repo | ||
48 | .SS Manifest (only) checkout options: | ||
49 | .TP | ||
50 | \fB\-\-current\-branch\fR | ||
51 | fetch only current manifest branch from server | ||
52 | .TP | ||
53 | \fB\-\-no\-current\-branch\fR | ||
54 | fetch all manifest branches from server | ||
55 | .TP | ||
56 | \fB\-\-tags\fR | ||
57 | fetch tags in the manifest | ||
58 | .TP | ||
59 | \fB\-\-no\-tags\fR | ||
60 | don't fetch tags in the manifest | ||
61 | .SS Checkout modes: | ||
62 | .TP | ||
63 | \fB\-\-mirror\fR | ||
64 | create a replica of the remote repositories rather | ||
65 | than a client working directory | ||
66 | .TP | ||
67 | \fB\-\-archive\fR | ||
68 | checkout an archive instead of a git repository for | ||
69 | each project. See git archive. | ||
70 | .TP | ||
71 | \fB\-\-worktree\fR | ||
72 | use git\-worktree to manage projects | ||
73 | .SS Project checkout optimizations: | ||
74 | .TP | ||
75 | \fB\-\-reference\fR=\fI\,DIR\/\fR | ||
76 | location of mirror directory | ||
77 | .TP | ||
78 | \fB\-\-dissociate\fR | ||
79 | dissociate from reference mirrors after clone | ||
80 | .TP | ||
81 | \fB\-\-depth\fR=\fI\,DEPTH\/\fR | ||
82 | create a shallow clone with given depth; see git clone | ||
83 | .TP | ||
84 | \fB\-\-partial\-clone\fR | ||
85 | perform partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code) | ||
86 | .TP | ||
87 | \fB\-\-no\-partial\-clone\fR | ||
88 | disable 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 | ||
91 | exclude the specified projects (a comma\-delimited | ||
92 | project names) from partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code) | ||
93 | .TP | ||
94 | \fB\-\-clone\-filter\fR=\fI\,CLONE_FILTER\/\fR | ||
95 | filter for use with \fB\-\-partial\-clone\fR [default: | ||
96 | blob:none] | ||
97 | .TP | ||
98 | \fB\-\-use\-superproject\fR | ||
99 | use the manifest superproject to sync projects | ||
100 | .TP | ||
101 | \fB\-\-no\-use\-superproject\fR | ||
102 | disable use of manifest superprojects | ||
103 | .TP | ||
104 | \fB\-\-clone\-bundle\fR | ||
105 | enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS (default if | ||
106 | not \fB\-\-partial\-clone\fR) | ||
107 | .TP | ||
108 | \fB\-\-no\-clone\-bundle\fR | ||
109 | disable 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 | ||
114 | repo repository location ($REPO_URL) | ||
115 | .TP | ||
116 | \fB\-\-repo\-rev\fR=\fI\,REV\/\fR | ||
117 | repo branch or revision ($REPO_REV) | ||
118 | .TP | ||
119 | \fB\-\-no\-repo\-verify\fR | ||
120 | do not verify repo source code | ||
121 | .SS Other options: | ||
122 | .TP | ||
123 | \fB\-\-config\-name\fR | ||
124 | Always 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 | ||
128 | Optional manifest file to use for this GITC client. | ||
129 | .TP | ||
130 | \fB\-c\fR GITC_CLIENT, \fB\-\-gitc\-client\fR=\fI\,GITC_CLIENT\/\fR | ||
131 | Name of the gitc_client instance to create or modify. | ||
132 | .PP | ||
133 | Run `repo help gitc\-init` to view the detailed manual. | ||
134 | .SH DETAILS | ||
135 | .PP | ||
136 | The 'repo gitc\-init' command is ran to initialize a new GITC client for use with | ||
137 | the GITC file system. | ||
138 | .PP | ||
139 | This command will setup the client directory, initialize repo, just like repo | ||
140 | init does, and then downloads the manifest collection and installs it in the | ||
141 | \&.repo/directory of the GITC client. | ||
142 | .PP | ||
143 | Once this is done, a GITC manifest is generated by pulling the HEAD SHA for each | ||
144 | project and generates the properly formatted XML file and installs it as | ||
145 | \&.manifest in the GITC client directory. | ||
146 | .PP | ||
147 | The \fB\-c\fR argument is required to specify the GITC client name. | ||
148 | .PP | ||
149 | The optional \fB\-f\fR argument can be used to specify the manifest file to use for | ||
150 | this 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 | ||
4 | repo \- 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 | ||
9 | Summary | ||
10 | .PP | ||
11 | Print lines matching a pattern | ||
12 | .SH OPTIONS | ||
13 | .TP | ||
14 | \fB\-h\fR, \fB\-\-help\fR | ||
15 | show this help message and exit | ||
16 | .TP | ||
17 | \fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR | ||
18 | number of jobs to run in parallel (default: based on | ||
19 | number of CPU cores) | ||
20 | .SS Logging options: | ||
21 | .TP | ||
22 | \fB\-\-verbose\fR | ||
23 | show all output | ||
24 | .TP | ||
25 | \fB\-q\fR, \fB\-\-quiet\fR | ||
26 | only show errors | ||
27 | .SS Sources: | ||
28 | .TP | ||
29 | \fB\-\-cached\fR | ||
30 | Search the index, instead of the work tree | ||
31 | .TP | ||
32 | \fB\-r\fR TREEish, \fB\-\-revision\fR=\fI\,TREEish\/\fR | ||
33 | Search TREEish, instead of the work tree | ||
34 | .SS Pattern: | ||
35 | .TP | ||
36 | \fB\-e\fR PATTERN | ||
37 | Pattern to search for | ||
38 | .TP | ||
39 | \fB\-i\fR, \fB\-\-ignore\-case\fR | ||
40 | Ignore case differences | ||
41 | .TP | ||
42 | \fB\-a\fR, \fB\-\-text\fR | ||
43 | Process binary files as if they were text | ||
44 | .TP | ||
45 | \fB\-I\fR | ||
46 | Don't match the pattern in binary files | ||
47 | .TP | ||
48 | \fB\-w\fR, \fB\-\-word\-regexp\fR | ||
49 | Match the pattern only at word boundaries | ||
50 | .TP | ||
51 | \fB\-v\fR, \fB\-\-invert\-match\fR | ||
52 | Select non\-matching lines | ||
53 | .TP | ||
54 | \fB\-G\fR, \fB\-\-basic\-regexp\fR | ||
55 | Use POSIX basic regexp for patterns (default) | ||
56 | .TP | ||
57 | \fB\-E\fR, \fB\-\-extended\-regexp\fR | ||
58 | Use POSIX extended regexp for patterns | ||
59 | .TP | ||
60 | \fB\-F\fR, \fB\-\-fixed\-strings\fR | ||
61 | Use fixed strings (not regexp) for pattern | ||
62 | .SS Pattern Grouping: | ||
63 | .TP | ||
64 | \fB\-\-all\-match\fR | ||
65 | Limit match to lines that have all patterns | ||
66 | .TP | ||
67 | \fB\-\-and\fR, \fB\-\-or\fR, \fB\-\-not\fR | ||
68 | Boolean operators to combine patterns | ||
69 | .TP | ||
70 | \-(, \-) | ||
71 | Boolean operator grouping | ||
72 | .SS Output: | ||
73 | .TP | ||
74 | \fB\-n\fR | ||
75 | Prefix the line number to matching lines | ||
76 | .TP | ||
77 | \fB\-C\fR CONTEXT | ||
78 | Show CONTEXT lines around match | ||
79 | .TP | ||
80 | \fB\-B\fR CONTEXT | ||
81 | Show CONTEXT lines before match | ||
82 | .TP | ||
83 | \fB\-A\fR CONTEXT | ||
84 | Show CONTEXT lines after match | ||
85 | .TP | ||
86 | \fB\-l\fR, \fB\-\-name\-only\fR, \fB\-\-files\-with\-matches\fR | ||
87 | Show only file names containing matching lines | ||
88 | .TP | ||
89 | \fB\-L\fR, \fB\-\-files\-without\-match\fR | ||
90 | Show only file names not containing matching lines | ||
91 | .PP | ||
92 | Run `repo help grep` to view the detailed manual. | ||
93 | .SH DETAILS | ||
94 | .PP | ||
95 | Search for the specified patterns in all project files. | ||
96 | .PP | ||
97 | Boolean Options | ||
98 | .PP | ||
99 | The following options can appear as often as necessary to express the pattern to | ||
100 | locate: | ||
101 | .HP | ||
102 | \fB\-e\fR PATTERN | ||
103 | .HP | ||
104 | \fB\-\-and\fR, \fB\-\-or\fR, \fB\-\-not\fR, \-(, \-) | ||
105 | .PP | ||
106 | Further, the \fB\-r\fR/\-\-revision option may be specified multiple times in order to | ||
107 | scan multiple trees. If the same file matches in more than one tree, only the | ||
108 | first result is reported, prefixed by the revision name it was found under. | ||
109 | .PP | ||
110 | Examples | ||
111 | .PP | ||
112 | Look for a line that has '#define' and either 'MAX_PATH or 'PATH_MAX': | ||
113 | .IP | ||
114 | repo grep \fB\-e\fR '#define' \fB\-\-and\fR \-\e( \fB\-e\fR MAX_PATH \fB\-e\fR PATH_MAX \e) | ||
115 | .PP | ||
116 | Look for a line that has 'NODE' or 'Unexpected' in files that contain a line | ||
117 | that matches both expressions: | ||
118 | .IP | ||
119 | repo 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 | ||
4 | repo \- repo help - manual page for repo help | ||
5 | .SH SYNOPSIS | ||
6 | .B repo | ||
7 | \fI\,help \/\fR[\fI\,--all|command\/\fR] | ||
8 | .SH DESCRIPTION | ||
9 | Summary | ||
10 | .PP | ||
11 | Display detailed help on a command | ||
12 | .SH OPTIONS | ||
13 | .TP | ||
14 | \fB\-h\fR, \fB\-\-help\fR | ||
15 | show this help message and exit | ||
16 | .TP | ||
17 | \fB\-a\fR, \fB\-\-all\fR | ||
18 | show the complete list of commands | ||
19 | .TP | ||
20 | \fB\-\-help\-all\fR | ||
21 | show the \fB\-\-help\fR of all commands | ||
22 | .SS Logging options: | ||
23 | .TP | ||
24 | \fB\-v\fR, \fB\-\-verbose\fR | ||
25 | show all output | ||
26 | .TP | ||
27 | \fB\-q\fR, \fB\-\-quiet\fR | ||
28 | only show errors | ||
29 | .PP | ||
30 | Run `repo help help` to view the detailed manual. | ||
31 | .SH DETAILS | ||
32 | .PP | ||
33 | Displays 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 | ||
4 | repo \- repo info - manual page for repo info | ||
5 | .SH SYNOPSIS | ||
6 | .B repo | ||
7 | \fI\,info \/\fR[\fI\,-dl\/\fR] [\fI\,-o \/\fR[\fI\,-c\/\fR]] [\fI\,<project>\/\fR...] | ||
8 | .SH DESCRIPTION | ||
9 | Summary | ||
10 | .PP | ||
11 | Get info on the manifest branch, current branch or unmerged branches | ||
12 | .SH OPTIONS | ||
13 | .TP | ||
14 | \fB\-h\fR, \fB\-\-help\fR | ||
15 | show this help message and exit | ||
16 | .TP | ||
17 | \fB\-d\fR, \fB\-\-diff\fR | ||
18 | show full info and commit diff including remote | ||
19 | branches | ||
20 | .TP | ||
21 | \fB\-o\fR, \fB\-\-overview\fR | ||
22 | show overview of all local commits | ||
23 | .TP | ||
24 | \fB\-c\fR, \fB\-\-current\-branch\fR | ||
25 | consider only checked out branches | ||
26 | .TP | ||
27 | \fB\-\-no\-current\-branch\fR | ||
28 | consider all local branches | ||
29 | .TP | ||
30 | \fB\-l\fR, \fB\-\-local\-only\fR | ||
31 | disable all remote operations | ||
32 | .SS Logging options: | ||
33 | .TP | ||
34 | \fB\-v\fR, \fB\-\-verbose\fR | ||
35 | show all output | ||
36 | .TP | ||
37 | \fB\-q\fR, \fB\-\-quiet\fR | ||
38 | only show errors | ||
39 | .PP | ||
40 | Run `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 | ||
4 | repo \- 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 | ||
9 | Summary | ||
10 | .PP | ||
11 | Initialize a repo client checkout in the current directory | ||
12 | .SH OPTIONS | ||
13 | .TP | ||
14 | \fB\-h\fR, \fB\-\-help\fR | ||
15 | show this help message and exit | ||
16 | .SS Logging options: | ||
17 | .TP | ||
18 | \fB\-v\fR, \fB\-\-verbose\fR | ||
19 | show all output | ||
20 | .TP | ||
21 | \fB\-q\fR, \fB\-\-quiet\fR | ||
22 | only show errors | ||
23 | .SS Manifest options: | ||
24 | .TP | ||
25 | \fB\-u\fR URL, \fB\-\-manifest\-url\fR=\fI\,URL\/\fR | ||
26 | manifest repository location | ||
27 | .TP | ||
28 | \fB\-b\fR REVISION, \fB\-\-manifest\-branch\fR=\fI\,REVISION\/\fR | ||
29 | manifest branch or revision (use HEAD for default) | ||
30 | .TP | ||
31 | \fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml | ||
32 | initial manifest file | ||
33 | .TP | ||
34 | \fB\-\-standalone\-manifest\fR | ||
35 | download the manifest as a static file rather then | ||
36 | create a git checkout of the manifest repo | ||
37 | .TP | ||
38 | \fB\-g\fR GROUP, \fB\-\-groups\fR=\fI\,GROUP\/\fR | ||
39 | restrict manifest projects to ones with specified | ||
40 | group(s) [default|all|G1,G2,G3|G4,\-G5,\-G6] | ||
41 | .TP | ||
42 | \fB\-p\fR PLATFORM, \fB\-\-platform\fR=\fI\,PLATFORM\/\fR | ||
43 | restrict manifest projects to ones with a specified | ||
44 | platform group [auto|all|none|linux|darwin|...] | ||
45 | .TP | ||
46 | \fB\-\-submodules\fR | ||
47 | sync any submodules associated with the manifest repo | ||
48 | .SS Manifest (only) checkout options: | ||
49 | .TP | ||
50 | \fB\-c\fR, \fB\-\-current\-branch\fR | ||
51 | fetch only current manifest branch from server | ||
52 | .TP | ||
53 | \fB\-\-no\-current\-branch\fR | ||
54 | fetch all manifest branches from server | ||
55 | .TP | ||
56 | \fB\-\-tags\fR | ||
57 | fetch tags in the manifest | ||
58 | .TP | ||
59 | \fB\-\-no\-tags\fR | ||
60 | don't fetch tags in the manifest | ||
61 | .SS Checkout modes: | ||
62 | .TP | ||
63 | \fB\-\-mirror\fR | ||
64 | create a replica of the remote repositories rather | ||
65 | than a client working directory | ||
66 | .TP | ||
67 | \fB\-\-archive\fR | ||
68 | checkout an archive instead of a git repository for | ||
69 | each project. See git archive. | ||
70 | .TP | ||
71 | \fB\-\-worktree\fR | ||
72 | use git\-worktree to manage projects | ||
73 | .SS Project checkout optimizations: | ||
74 | .TP | ||
75 | \fB\-\-reference\fR=\fI\,DIR\/\fR | ||
76 | location of mirror directory | ||
77 | .TP | ||
78 | \fB\-\-dissociate\fR | ||
79 | dissociate from reference mirrors after clone | ||
80 | .TP | ||
81 | \fB\-\-depth\fR=\fI\,DEPTH\/\fR | ||
82 | create a shallow clone with given depth; see git clone | ||
83 | .TP | ||
84 | \fB\-\-partial\-clone\fR | ||
85 | perform partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code) | ||
86 | .TP | ||
87 | \fB\-\-no\-partial\-clone\fR | ||
88 | disable 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 | ||
91 | exclude the specified projects (a comma\-delimited | ||
92 | project names) from partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code) | ||
93 | .TP | ||
94 | \fB\-\-clone\-filter\fR=\fI\,CLONE_FILTER\/\fR | ||
95 | filter for use with \fB\-\-partial\-clone\fR [default: | ||
96 | blob:none] | ||
97 | .TP | ||
98 | \fB\-\-use\-superproject\fR | ||
99 | use the manifest superproject to sync projects | ||
100 | .TP | ||
101 | \fB\-\-no\-use\-superproject\fR | ||
102 | disable use of manifest superprojects | ||
103 | .TP | ||
104 | \fB\-\-clone\-bundle\fR | ||
105 | enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS (default if | ||
106 | not \fB\-\-partial\-clone\fR) | ||
107 | .TP | ||
108 | \fB\-\-no\-clone\-bundle\fR | ||
109 | disable 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 | ||
114 | repo repository location ($REPO_URL) | ||
115 | .TP | ||
116 | \fB\-\-repo\-rev\fR=\fI\,REV\/\fR | ||
117 | repo branch or revision ($REPO_REV) | ||
118 | .TP | ||
119 | \fB\-\-no\-repo\-verify\fR | ||
120 | do not verify repo source code | ||
121 | .SS Other options: | ||
122 | .TP | ||
123 | \fB\-\-config\-name\fR | ||
124 | Always prompt for name/e\-mail | ||
125 | .PP | ||
126 | Run `repo help init` to view the detailed manual. | ||
127 | .SH DETAILS | ||
128 | .PP | ||
129 | The 'repo init' command is run once to install and initialize repo. The latest | ||
130 | repo source code and manifest collection is downloaded from the server and is | ||
131 | installed in the .repo/ directory in the current working directory. | ||
132 | .PP | ||
133 | When creating a new checkout, the manifest URL is the only required setting. It | ||
134 | may be specified using the \fB\-\-manifest\-url\fR option, or as the first optional | ||
135 | argument. | ||
136 | .PP | ||
137 | The optional \fB\-b\fR argument can be used to select the manifest branch to checkout | ||
138 | and use. If no branch is specified, the remote's default branch is used. This is | ||
139 | equivalent to using \fB\-b\fR HEAD. | ||
140 | .PP | ||
141 | The optional \fB\-m\fR argument can be used to specify an alternate manifest to be | ||
142 | used. If no manifest is specified, the manifest default.xml will be used. | ||
143 | .PP | ||
144 | If the \fB\-\-standalone\-manifest\fR argument is set, the manifest will be downloaded | ||
145 | directly from the specified \fB\-\-manifest\-url\fR as a static file (rather than setting | ||
146 | up a manifest git checkout). With \fB\-\-standalone\-manifest\fR, the manifest will be | ||
147 | fully static and will not be re\-downloaded during subsesquent `repo init` and | ||
148 | `repo sync` calls. | ||
149 | .PP | ||
150 | The \fB\-\-reference\fR option can be used to point to a directory that has the content | ||
151 | of a \fB\-\-mirror\fR sync. This will make the working directory use as much data as | ||
152 | possible from the local reference directory when fetching from the server. This | ||
153 | will make the sync go a lot faster by reducing data traffic on the network. | ||
154 | .PP | ||
155 | The \fB\-\-dissociate\fR option can be used to borrow the objects from the directory | ||
156 | specified with the \fB\-\-reference\fR option only to reduce network transfer, and stop | ||
157 | borrowing from them after a first clone is made by making necessary local copies | ||
158 | of borrowed objects. | ||
159 | .PP | ||
160 | The \fB\-\-no\-clone\-bundle\fR option disables any attempt to use \fI\,$URL/clone.bundle\/\fP to | ||
161 | bootstrap a new Git repository from a resumeable bundle file on a content | ||
162 | delivery network. This may be necessary if there are problems with the local | ||
163 | Python HTTP client or proxy configuration, but the Git binary works. | ||
164 | .PP | ||
165 | Switching Manifest Branches | ||
166 | .PP | ||
167 | To switch to another manifest branch, `repo init \fB\-b\fR otherbranch` may be used in | ||
168 | an 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 | ||
170 | files. | ||
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 | ||
4 | repo \- 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 | ||
9 | Summary | ||
10 | .PP | ||
11 | List projects and their associated directories | ||
12 | .PP | ||
13 | repo list [\-f] \fB\-r\fR str1 [str2]... | ||
14 | .SH OPTIONS | ||
15 | .TP | ||
16 | \fB\-h\fR, \fB\-\-help\fR | ||
17 | show this help message and exit | ||
18 | .TP | ||
19 | \fB\-r\fR, \fB\-\-regex\fR | ||
20 | filter the project list based on regex or wildcard | ||
21 | matching of strings | ||
22 | .TP | ||
23 | \fB\-g\fR GROUPS, \fB\-\-groups\fR=\fI\,GROUPS\/\fR | ||
24 | filter the project list based on the groups the | ||
25 | project is in | ||
26 | .TP | ||
27 | \fB\-a\fR, \fB\-\-all\fR | ||
28 | show projects regardless of checkout state | ||
29 | .TP | ||
30 | \fB\-n\fR, \fB\-\-name\-only\fR | ||
31 | display only the name of the repository | ||
32 | .TP | ||
33 | \fB\-p\fR, \fB\-\-path\-only\fR | ||
34 | display only the path of the repository | ||
35 | .TP | ||
36 | \fB\-f\fR, \fB\-\-fullpath\fR | ||
37 | display the full work tree path instead of the | ||
38 | relative path | ||
39 | .TP | ||
40 | \fB\-\-relative\-to\fR=\fI\,PATH\/\fR | ||
41 | display paths relative to this one (default: top of | ||
42 | repo client checkout) | ||
43 | .SS Logging options: | ||
44 | .TP | ||
45 | \fB\-v\fR, \fB\-\-verbose\fR | ||
46 | show all output | ||
47 | .TP | ||
48 | \fB\-q\fR, \fB\-\-quiet\fR | ||
49 | only show errors | ||
50 | .PP | ||
51 | Run `repo help list` to view the detailed manual. | ||
52 | .SH DETAILS | ||
53 | .PP | ||
54 | List all projects; pass '.' to list the project for the cwd. | ||
55 | .PP | ||
56 | By default, only projects that currently exist in the checkout are shown. If you | ||
57 | want to list all projects (using the specified filter settings), use the \fB\-\-all\fR | ||
58 | option. If you want to show all projects regardless of the manifest groups, then | ||
59 | also pass \fB\-\-groups\fR all. | ||
60 | .PP | ||
61 | This 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 | ||
4 | repo \- 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 | ||
9 | Summary | ||
10 | .PP | ||
11 | Manifest inspection utility | ||
12 | .SH OPTIONS | ||
13 | .TP | ||
14 | \fB\-h\fR, \fB\-\-help\fR | ||
15 | show this help message and exit | ||
16 | .TP | ||
17 | \fB\-r\fR, \fB\-\-revision\-as\-HEAD\fR | ||
18 | save revisions as current HEAD | ||
19 | .TP | ||
20 | \fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml | ||
21 | temporary manifest to use for this sync | ||
22 | .TP | ||
23 | \fB\-\-suppress\-upstream\-revision\fR | ||
24 | if in \fB\-r\fR mode, do not write the upstream field (only | ||
25 | of use if the branch names for a sha1 manifest are | ||
26 | sensitive) | ||
27 | .TP | ||
28 | \fB\-\-suppress\-dest\-branch\fR | ||
29 | if in \fB\-r\fR mode, do not write the dest\-branch field | ||
30 | (only of use if the branch names for a sha1 manifest | ||
31 | are sensitive) | ||
32 | .TP | ||
33 | \fB\-\-json\fR | ||
34 | output manifest in JSON format (experimental) | ||
35 | .TP | ||
36 | \fB\-\-pretty\fR | ||
37 | format output for humans to read | ||
38 | .TP | ||
39 | \fB\-\-no\-local\-manifests\fR | ||
40 | ignore local manifests | ||
41 | .TP | ||
42 | \fB\-o\fR \-|NAME.xml, \fB\-\-output\-file\fR=\fI\,\-\/\fR|NAME.xml | ||
43 | file to save the manifest to | ||
44 | .SS Logging options: | ||
45 | .TP | ||
46 | \fB\-v\fR, \fB\-\-verbose\fR | ||
47 | show all output | ||
48 | .TP | ||
49 | \fB\-q\fR, \fB\-\-quiet\fR | ||
50 | only show errors | ||
51 | .PP | ||
52 | Run `repo help manifest` to view the detailed manual. | ||
53 | .SH DETAILS | ||
54 | .PP | ||
55 | With the \fB\-o\fR option, exports the current manifest for inspection. The manifest | ||
56 | and (if present) local_manifests/ are combined together to produce a single | ||
57 | manifest file. This file can be stored in a Git repository for use during future | ||
58 | \&'repo init' invocations. | ||
59 | .PP | ||
60 | The \fB\-r\fR option can be used to generate a manifest file with project revisions set | ||
61 | to the current commit hash. These are known as "revision locked manifests", as | ||
62 | they don't follow a particular branch. In this case, the 'upstream' attribute is | ||
63 | set to the ref we were on when the manifest was generated. The 'dest\-branch' | ||
64 | attribute is set to indicate the remote ref to push changes to via 'repo | ||
65 | upload'. | ||
66 | .PP | ||
67 | repo Manifest Format | ||
68 | .PP | ||
69 | A repo manifest describes the structure of a repo client; that is the | ||
70 | directories that are visible and where they should be obtained from with git. | ||
71 | .PP | ||
72 | The 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 | ||
75 | Manifests are inherently version controlled, since they are kept within a Git | ||
76 | repository. Updates to manifests are automatically obtained by clients during | ||
77 | `repo sync`. | ||
78 | .PP | ||
79 | [TOC] | ||
80 | .PP | ||
81 | XML File Format | ||
82 | .PP | ||
83 | A 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?, | ||
88 | remote*, | ||
89 | default?, | ||
90 | manifest\-server?, | ||
91 | remove\-project*, | ||
92 | project*, | ||
93 | extend\-project*, | ||
94 | repo\-hooks?, | ||
95 | superproject?, | ||
96 | contactinfo?, | ||
97 | include*)> | ||
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*, | ||
123 | project*, | ||
124 | copyfile*, | ||
125 | linkfile*)> | ||
126 | .TP | ||
127 | <!ATTLIST project name | ||
128 | CDATA #REQUIRED> | ||
129 | .TP | ||
130 | <!ATTLIST project path | ||
131 | CDATA #IMPLIED> | ||
132 | .TP | ||
133 | <!ATTLIST project remote | ||
134 | IDREF #IMPLIED> | ||
135 | .TP | ||
136 | <!ATTLIST project revision | ||
137 | CDATA #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 | ||
190 | For compatibility purposes across repo releases, all unknown elements are | ||
191 | silently ignored. However, repo reserves all possible names for itself for | ||
192 | future use. If you want to use custom elements, the `x\-*` namespace is reserved | ||
193 | for that purpose, and repo guarantees to never allocate any corresponding names. | ||
194 | .PP | ||
195 | A description of the elements and their attributes follows. | ||
196 | .PP | ||
197 | Element manifest | ||
198 | .PP | ||
199 | The root element of the file. | ||
200 | .PP | ||
201 | Element notice | ||
202 | .PP | ||
203 | Arbitrary text that is displayed to users whenever `repo sync` finishes. The | ||
204 | content is simply passed through as it exists in the manifest. | ||
205 | .PP | ||
206 | Element remote | ||
207 | .PP | ||
208 | One or more remote elements may be specified. Each remote element specifies a | ||
209 | Git URL shared by one or more projects and (optionally) the Gerrit review server | ||
210 | those projects upload changes through. | ||
211 | .PP | ||
212 | Attribute `name`: A short name unique to this manifest file. The name specified | ||
213 | here is used as the remote name in each project's .git/config, and is therefore | ||
214 | automatically available to commands like `git fetch`, `git remote`, `git pull` | ||
215 | and `git push`. | ||
216 | .PP | ||
217 | Attribute `alias`: The alias, if specified, is used to override `name` to be set | ||
218 | as the remote name in each project's .git/config. Its value can be duplicated | ||
219 | while attribute `name` has to be unique in the manifest file. This helps each | ||
220 | project to be able to have same remote name which actually points to different | ||
221 | remote url. | ||
222 | .PP | ||
223 | Attribute `fetch`: The Git URL prefix for all projects which use this remote. | ||
224 | Each project's name is appended to this prefix to form the actual URL used to | ||
225 | clone the project. | ||
226 | .PP | ||
227 | Attribute `pushurl`: The Git "push" URL prefix for all projects which use this | ||
228 | remote. Each project's name is appended to this prefix to form the actual URL | ||
229 | used to "git push" the project. This attribute is optional; if not specified | ||
230 | then "git push" will use the same URL as the `fetch` attribute. | ||
231 | .PP | ||
232 | Attribute `review`: Hostname of the Gerrit server where reviews are uploaded to | ||
233 | by `repo upload`. This attribute is optional; if not specified then `repo | ||
234 | upload` will not function. | ||
235 | .PP | ||
236 | Attribute `revision`: Name of a Git branch (e.g. `main` or `refs/heads/main`). | ||
237 | Remotes with their own revision will override the default revision. | ||
238 | .PP | ||
239 | Element default | ||
240 | .PP | ||
241 | At most one default element may be specified. Its remote and revision attributes | ||
242 | are used when a project element does not specify its own remote or revision | ||
243 | attribute. | ||
244 | .PP | ||
245 | Attribute `remote`: Name of a previously defined remote element. Project | ||
246 | elements lacking a remote attribute of their own will use this remote. | ||
247 | .PP | ||
248 | Attribute `revision`: Name of a Git branch (e.g. `main` or `refs/heads/main`). | ||
249 | Project elements lacking their own revision attribute will use this revision. | ||
250 | .PP | ||
251 | Attribute `dest\-branch`: Name of a Git branch (e.g. `main`). Project elements | ||
252 | not setting their own `dest\-branch` will inherit this value. If this value is | ||
253 | not set, projects will use `revision` by default instead. | ||
254 | .PP | ||
255 | Attribute `upstream`: Name of the Git ref in which a sha1 can be found. Used | ||
256 | when syncing a revision locked manifest in \fB\-c\fR mode to avoid having to sync the | ||
257 | entire ref space. Project elements not setting their own `upstream` will inherit | ||
258 | this value. | ||
259 | .PP | ||
260 | Attribute `sync\-j`: Number of parallel jobs to use when synching. | ||
261 | .PP | ||
262 | Attribute `sync\-c`: Set to true to only sync the given Git branch (specified in | ||
263 | the `revision` attribute) rather than the whole ref space. Project elements | ||
264 | lacking a sync\-c element of their own will use this value. | ||
265 | .PP | ||
266 | Attribute `sync\-s`: Set to true to also sync sub\-projects. | ||
267 | .PP | ||
268 | Attribute `sync\-tags`: Set to false to only sync the given Git branch (specified | ||
269 | in the `revision` attribute) rather than the other ref tags. | ||
270 | .PP | ||
271 | Element manifest\-server | ||
272 | .PP | ||
273 | At most one manifest\-server may be specified. The url attribute is used to | ||
274 | specify the URL of a manifest server, which is an XML RPC service. | ||
275 | .PP | ||
276 | The manifest server should implement the following RPC methods: | ||
277 | .IP | ||
278 | GetApprovedManifest(branch, target) | ||
279 | .PP | ||
280 | Return a manifest in which each project is pegged to a known good revision for | ||
281 | the current branch and target. This is used by repo sync when the \fB\-\-smart\-sync\fR | ||
282 | option is given. | ||
283 | .PP | ||
284 | The target to use is defined by environment variables TARGET_PRODUCT and | ||
285 | TARGET_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 | ||
287 | variables or both are not present, the program will call GetApprovedManifest | ||
288 | without the target parameter and the manifest server should choose a reasonable | ||
289 | default target. | ||
290 | .IP | ||
291 | GetManifest(tag) | ||
292 | .PP | ||
293 | Return a manifest in which each project is pegged to the revision at the | ||
294 | specified tag. This is used by repo sync when the \fB\-\-smart\-tag\fR option is given. | ||
295 | .PP | ||
296 | Element project | ||
297 | .PP | ||
298 | One or more project elements may be specified. Each element describes a single | ||
299 | Git repository to be cloned into the repo client workspace. You may specify | ||
300 | Git\-submodules by creating a nested project. Git\-submodules will be | ||
301 | automatically recognized and inherit their parent's attributes, but those may be | ||
302 | overridden by an explicitly specified project element. | ||
303 | .PP | ||
304 | Attribute `name`: A unique name for this project. The project's name is appended | ||
305 | onto its remote's fetch URL to generate the actual URL to configure the Git | ||
306 | remote with. The URL gets formed as: | ||
307 | .IP | ||
308 | ${remote_fetch}/${project_name}.git | ||
309 | .PP | ||
310 | where ${remote_fetch} is the remote's fetch attribute and ${project_name} is the | ||
311 | project's name attribute. The suffix ".git" is always appended as repo assumes | ||
312 | the upstream is a forest of bare Git repositories. If the project has a parent | ||
313 | element, its name will be prefixed by the parent's. | ||
314 | .PP | ||
315 | The project name must match the name Gerrit knows, if Gerrit is being used for | ||
316 | code reviews. | ||
317 | .PP | ||
318 | "name" must not be empty, and may not be an absolute path or use "." or ".." | ||
319 | path components. It is always interpreted relative to the remote's fetch | ||
320 | settings, so if a different base path is needed, declare a different remote with | ||
321 | the new settings needed. These restrictions are not enforced for [Local | ||
322 | Manifests]. | ||
323 | .PP | ||
324 | Attribute `path`: An optional path relative to the top directory of the repo | ||
325 | client where the Git working directory for this project should be placed. If not | ||
326 | supplied the project "name" is used. If the project has a parent element, its | ||
327 | path will be prefixed by the parent's. | ||
328 | .PP | ||
329 | "path" may not be an absolute path or use "." or ".." path components. These | ||
330 | restrictions are not enforced for [Local Manifests]. | ||
331 | .PP | ||
332 | If you want to place files into the root of the checkout (e.g. a README or | ||
333 | Makefile or another build script), use the [copyfile] or [linkfile] elements | ||
334 | instead. | ||
335 | .PP | ||
336 | Attribute `remote`: Name of a previously defined remote element. If not supplied | ||
337 | the remote given by the default element is used. | ||
338 | .PP | ||
339 | Attribute `revision`: Name of the Git branch the manifest wants to track for | ||
340 | this 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 | ||
342 | have not been extensively tested. If not supplied the revision given by the | ||
343 | remote element is used if applicable, else the default element is used. | ||
344 | .PP | ||
345 | Attribute `dest\-branch`: Name of a Git branch (e.g. `main`). When using `repo | ||
346 | upload`, changes will be submitted for code review on this branch. If | ||
347 | unspecified both here and in the default element, `revision` is used instead. | ||
348 | .PP | ||
349 | Attribute `groups`: List of groups to which this project belongs, whitespace or | ||
350 | comma separated. All projects belong to the group "all", and each project | ||
351 | automatically 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 | ||
353 | implicitly in the following manifest groups: default, name:monkeys, and | ||
354 | path:barrel\-of. If you place a project in the group "notdefault", it will not be | ||
355 | automatically downloaded by repo. If the project has a parent element, the | ||
356 | `name` and `path` here are the prefixed ones. | ||
357 | .PP | ||
358 | Attribute `sync\-c`: Set to true to only sync the given Git branch (specified in | ||
359 | the `revision` attribute) rather than the whole ref space. | ||
360 | .PP | ||
361 | Attribute `sync\-s`: Set to true to also sync sub\-projects. | ||
362 | .PP | ||
363 | Attribute `upstream`: Name of the Git ref in which a sha1 can be found. Used | ||
364 | when syncing a revision locked manifest in \fB\-c\fR mode to avoid having to sync the | ||
365 | entire ref space. | ||
366 | .PP | ||
367 | Attribute `clone\-depth`: Set the depth to use when fetching this project. If | ||
368 | specified, this value will override any value given to repo init with the | ||
369 | \fB\-\-depth\fR option on the command line. | ||
370 | .PP | ||
371 | Attribute `force\-path`: Set to true to force this project to create the local | ||
372 | mirror repository according to its `path` attribute (if supplied) rather than | ||
373 | the `name` attribute. This attribute only applies to the local mirrors syncing, | ||
374 | it will be ignored when syncing the projects in a client working directory. | ||
375 | .PP | ||
376 | Element extend\-project | ||
377 | .PP | ||
378 | Modify the attributes of the named project. | ||
379 | .PP | ||
380 | This element is mostly useful in a local manifest file, to modify the attributes | ||
381 | of an existing project without completely replacing the existing project | ||
382 | definition. This makes the local manifest more robust against changes to the | ||
383 | original manifest. | ||
384 | .PP | ||
385 | Attribute `path`: If specified, limit the change to projects checked out at the | ||
386 | specified path, rather than all projects with the given name. | ||
387 | .PP | ||
388 | Attribute `groups`: List of additional groups to which this project belongs. | ||
389 | Same syntax as the corresponding element of `project`. | ||
390 | .PP | ||
391 | Attribute `revision`: If specified, overrides the revision of the original | ||
392 | project. Same syntax as the corresponding element of `project`. | ||
393 | .PP | ||
394 | Attribute `remote`: If specified, overrides the remote of the original project. | ||
395 | Same syntax as the corresponding element of `project`. | ||
396 | .PP | ||
397 | Element annotation | ||
398 | .PP | ||
399 | Zero or more annotation elements may be specified as children of a project or | ||
400 | remote element. Each element describes a name\-value pair. For projects, this | ||
401 | name\-value pair will be exported into each project's environment during a | ||
402 | \&'forall' command, prefixed with `REPO__`. In addition, there is an optional | ||
403 | attribute "keep" which accepts the case insensitive values "true" (default) or | ||
404 | "false". This attribute determines whether or not the annotation will be kept | ||
405 | when exported with the manifest subcommand. | ||
406 | .PP | ||
407 | Element copyfile | ||
408 | .PP | ||
409 | Zero or more copyfile elements may be specified as children of a project | ||
410 | element. Each element describes a src\-dest pair of files; the "src" file will be | ||
411 | copied 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 | ||
414 | from paths outside of the project or to paths outside of the repo client is not | ||
415 | allowed. | ||
416 | .PP | ||
417 | "src" and "dest" must be files. Directories or symlinks are not allowed. | ||
418 | Intermediate paths must not be symlinks either. | ||
419 | .PP | ||
420 | Parent directories of "dest" will be automatically created if missing. | ||
421 | .PP | ||
422 | Element linkfile | ||
423 | .PP | ||
424 | It's just like copyfile and runs at the same time as copyfile but instead of | ||
425 | copying it creates a symlink. | ||
426 | .PP | ||
427 | The symlink is created at "dest" (relative to the top of the tree) and points to | ||
428 | the path specified by "src" which is a path in the project. | ||
429 | .PP | ||
430 | Parent directories of "dest" will be automatically created if missing. | ||
431 | .PP | ||
432 | The symlink target may be a file or directory, but it may not point outside of | ||
433 | the repo client. | ||
434 | .PP | ||
435 | Element remove\-project | ||
436 | .PP | ||
437 | Deletes the named project from the internal manifest table, possibly allowing a | ||
438 | subsequent project element in the same manifest file to replace the project with | ||
439 | a different source. | ||
440 | .PP | ||
441 | This element is mostly useful in a local manifest file, where the user can | ||
442 | remove a project, and possibly replace it with their own definition. | ||
443 | .PP | ||
444 | Attribute `optional`: Set to true to ignore remove\-project elements with no | ||
445 | matching `project` element. | ||
446 | .PP | ||
447 | Element repo\-hooks | ||
448 | .PP | ||
449 | NB: See the [practical documentation](./repo\-hooks.md) for using repo hooks. | ||
450 | .PP | ||
451 | Only one repo\-hooks element may be specified at a time. Attempting to redefine | ||
452 | it will fail to parse. | ||
453 | .PP | ||
454 | Attribute `in\-project`: The project where the hooks are defined. The value must | ||
455 | match the `name` attribute (**not** the `path` attribute) of a previously | ||
456 | defined `project` element. | ||
457 | .PP | ||
458 | Attribute `enabled\-list`: List of hooks to use, whitespace or comma separated. | ||
459 | .PP | ||
460 | Element superproject | ||
461 | .PP | ||
462 | *** *Note*: This is currently a WIP. *** | ||
463 | .PP | ||
464 | NB: See the [git superprojects documentation]( | ||
465 | https://en.wikibooks.org/wiki/Git/Submodules_and_Superprojects) for background | ||
466 | information. | ||
467 | .PP | ||
468 | This 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 | ||
470 | defaults. At most one superproject may be specified. Attempting to redefine it | ||
471 | will fail to parse. | ||
472 | .PP | ||
473 | Attribute `name`: A unique name for the superproject. This attribute has the | ||
474 | same meaning as project's name attribute. See the [element | ||
475 | project](#element\-project) for more information. | ||
476 | .PP | ||
477 | Attribute `remote`: Name of a previously defined remote element. If not supplied | ||
478 | the remote given by the default element is used. | ||
479 | .PP | ||
480 | Element contactinfo | ||
481 | .PP | ||
482 | *** *Note*: This is currently a WIP. *** | ||
483 | .PP | ||
484 | This 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 | ||
486 | entries will clobber earlier ones. This would allow manifest authors who extend | ||
487 | manifests to specify their own contact info. | ||
488 | .PP | ||
489 | Attribute `bugurl`: The URL to file a bug against the manifest owner. | ||
490 | .PP | ||
491 | Element include | ||
492 | .PP | ||
493 | This element provides the capability of including another manifest file into the | ||
494 | originating manifest. Normal rules apply for the target manifest to include \- it | ||
495 | must be a usable manifest on its own. | ||
496 | .PP | ||
497 | Attribute `name`: the manifest to include, specified relative to the manifest | ||
498 | repository's root. | ||
499 | .PP | ||
500 | "name" may not be an absolute path or use "." or ".." path components. These | ||
501 | restrictions are not enforced for [Local Manifests]. | ||
502 | .PP | ||
503 | Attribute `groups`: List of additional groups to which all projects in the | ||
504 | included manifest belong. This appends and recurses, meaning all projects in | ||
505 | sub\-manifests carry all parent include groups. Same syntax as the corresponding | ||
506 | element of `project`. | ||
507 | .PP | ||
508 | Local Manifests | ||
509 | .PP | ||
510 | Additional remotes and projects may be added through local manifest files stored | ||
511 | in `$TOP_DIR/.repo/local_manifests/*.xml`. | ||
512 | .PP | ||
513 | For example: | ||
514 | .IP | ||
515 | \f(CW$ ls .repo/local_manifests\fR | ||
516 | .IP | ||
517 | local_manifest.xml | ||
518 | another_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 | ||
527 | name="tools/manifest" /> | ||
528 | .IP | ||
529 | <project path="platform\-manifest" | ||
530 | .IP | ||
531 | name="platform/manifest" /> | ||
532 | .IP | ||
533 | </manifest> | ||
534 | .PP | ||
535 | Users may add projects to the local manifest(s) prior to a `repo sync` | ||
536 | invocation, instructing repo to automatically download and manage these extra | ||
537 | projects. | ||
538 | .PP | ||
539 | Manifest files stored in `$TOP_DIR/.repo/local_manifests/*.xml` will be loaded | ||
540 | in alphabetical order. | ||
541 | .PP | ||
542 | Projects from local manifest files are added into local::<local manifest | ||
543 | filename> group. | ||
544 | .PP | ||
545 | The 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 | ||
4 | repo \- 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 | ||
9 | Summary | ||
10 | .PP | ||
11 | Display overview of unmerged project branches | ||
12 | .SH OPTIONS | ||
13 | .TP | ||
14 | \fB\-h\fR, \fB\-\-help\fR | ||
15 | show this help message and exit | ||
16 | .TP | ||
17 | \fB\-c\fR, \fB\-\-current\-branch\fR | ||
18 | consider only checked out branches | ||
19 | .TP | ||
20 | \fB\-\-no\-current\-branch\fR | ||
21 | consider all local branches | ||
22 | .SS Logging options: | ||
23 | .TP | ||
24 | \fB\-v\fR, \fB\-\-verbose\fR | ||
25 | show all output | ||
26 | .TP | ||
27 | \fB\-q\fR, \fB\-\-quiet\fR | ||
28 | only show errors | ||
29 | .PP | ||
30 | Run `repo help overview` to view the detailed manual. | ||
31 | .SH DETAILS | ||
32 | .PP | ||
33 | The 'repo overview' command is used to display an overview of the projects | ||
34 | branches, and list any local commits that have not yet been merged into the | ||
35 | project. | ||
36 | .PP | ||
37 | The \fB\-c\fR/\-\-current\-branch option can be used to restrict the output to only | ||
38 | branches currently checked out in each project. By default, all branches are | ||
39 | displayed. | ||
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 | ||
4 | repo \- repo prune - manual page for repo prune | ||
5 | .SH SYNOPSIS | ||
6 | .B repo | ||
7 | \fI\,prune \/\fR[\fI\,<project>\/\fR...] | ||
8 | .SH DESCRIPTION | ||
9 | Summary | ||
10 | .PP | ||
11 | Prune (delete) already merged topics | ||
12 | .SH OPTIONS | ||
13 | .TP | ||
14 | \fB\-h\fR, \fB\-\-help\fR | ||
15 | show this help message and exit | ||
16 | .TP | ||
17 | \fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR | ||
18 | number of jobs to run in parallel (default: based on | ||
19 | number of CPU cores) | ||
20 | .SS Logging options: | ||
21 | .TP | ||
22 | \fB\-v\fR, \fB\-\-verbose\fR | ||
23 | show all output | ||
24 | .TP | ||
25 | \fB\-q\fR, \fB\-\-quiet\fR | ||
26 | only show errors | ||
27 | .PP | ||
28 | Run `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 | ||
4 | repo \- 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 | ||
9 | Summary | ||
10 | .PP | ||
11 | Rebase local branches on upstream branch | ||
12 | .SH OPTIONS | ||
13 | .TP | ||
14 | \fB\-h\fR, \fB\-\-help\fR | ||
15 | show this help message and exit | ||
16 | .TP | ||
17 | \fB\-\-fail\-fast\fR | ||
18 | stop rebasing after first error is hit | ||
19 | .TP | ||
20 | \fB\-f\fR, \fB\-\-force\-rebase\fR | ||
21 | pass \fB\-\-force\-rebase\fR to git rebase | ||
22 | .TP | ||
23 | \fB\-\-no\-ff\fR | ||
24 | pass \fB\-\-no\-ff\fR to git rebase | ||
25 | .TP | ||
26 | \fB\-\-autosquash\fR | ||
27 | pass \fB\-\-autosquash\fR to git rebase | ||
28 | .TP | ||
29 | \fB\-\-whitespace\fR=\fI\,WS\/\fR | ||
30 | pass \fB\-\-whitespace\fR to git rebase | ||
31 | .TP | ||
32 | \fB\-\-auto\-stash\fR | ||
33 | stash local modifications before starting | ||
34 | .TP | ||
35 | \fB\-m\fR, \fB\-\-onto\-manifest\fR | ||
36 | rebase onto the manifest version instead of upstream | ||
37 | HEAD (this helps to make sure the local tree stays | ||
38 | consistent if you previously synced to a manifest) | ||
39 | .SS Logging options: | ||
40 | .TP | ||
41 | \fB\-v\fR, \fB\-\-verbose\fR | ||
42 | show all output | ||
43 | .TP | ||
44 | \fB\-q\fR, \fB\-\-quiet\fR | ||
45 | only show errors | ||
46 | .TP | ||
47 | \fB\-i\fR, \fB\-\-interactive\fR | ||
48 | interactive rebase (single project only) | ||
49 | .PP | ||
50 | Run `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 | ||
54 | to the HEAD of the upstream history, useful when you have made commits in a | ||
55 | topic 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 | ||
4 | repo \- repo selfupdate - manual page for repo selfupdate | ||
5 | .SH SYNOPSIS | ||
6 | .B repo | ||
7 | \fI\,selfupdate\/\fR | ||
8 | .SH DESCRIPTION | ||
9 | Summary | ||
10 | .PP | ||
11 | Update repo to the latest version | ||
12 | .SH OPTIONS | ||
13 | .TP | ||
14 | \fB\-h\fR, \fB\-\-help\fR | ||
15 | show this help message and exit | ||
16 | .SS Logging options: | ||
17 | .TP | ||
18 | \fB\-v\fR, \fB\-\-verbose\fR | ||
19 | show all output | ||
20 | .TP | ||
21 | \fB\-q\fR, \fB\-\-quiet\fR | ||
22 | only show errors | ||
23 | .SS repo Version options: | ||
24 | .TP | ||
25 | \fB\-\-no\-repo\-verify\fR | ||
26 | do not verify repo source code | ||
27 | .PP | ||
28 | Run `repo help selfupdate` to view the detailed manual. | ||
29 | .SH DETAILS | ||
30 | .PP | ||
31 | The 'repo selfupdate' command upgrades repo to the latest version, if a newer | ||
32 | version is available. | ||
33 | .PP | ||
34 | Normally this is done automatically by 'repo sync' and does not need to be | ||
35 | performed 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 | ||
4 | repo \- repo smartsync - manual page for repo smartsync | ||
5 | .SH SYNOPSIS | ||
6 | .B repo | ||
7 | \fI\,smartsync \/\fR[\fI\,<project>\/\fR...] | ||
8 | .SH DESCRIPTION | ||
9 | Summary | ||
10 | .PP | ||
11 | Update working tree to the latest known good revision | ||
12 | .SH OPTIONS | ||
13 | .TP | ||
14 | \fB\-h\fR, \fB\-\-help\fR | ||
15 | show this help message and exit | ||
16 | .TP | ||
17 | \fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR | ||
18 | number of jobs to run in parallel (default: based on | ||
19 | number of CPU cores) | ||
20 | .TP | ||
21 | \fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR | ||
22 | number of network jobs to run in parallel (defaults to | ||
23 | \fB\-\-jobs\fR) | ||
24 | .TP | ||
25 | \fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR | ||
26 | number of local checkout jobs to run in parallel | ||
27 | (defaults to \fB\-\-jobs\fR) | ||
28 | .TP | ||
29 | \fB\-f\fR, \fB\-\-force\-broken\fR | ||
30 | obsolete option (to be deleted in the future) | ||
31 | .TP | ||
32 | \fB\-\-fail\-fast\fR | ||
33 | stop syncing after first error is hit | ||
34 | .TP | ||
35 | \fB\-\-force\-sync\fR | ||
36 | overwrite an existing git directory if it needs to | ||
37 | point to a different object directory. WARNING: this | ||
38 | may cause loss of data | ||
39 | .TP | ||
40 | \fB\-\-force\-remove\-dirty\fR | ||
41 | force remove projects with uncommitted modifications | ||
42 | if projects no longer exist in the manifest. WARNING: | ||
43 | this may cause loss of data | ||
44 | .TP | ||
45 | \fB\-l\fR, \fB\-\-local\-only\fR | ||
46 | only update working tree, don't fetch | ||
47 | .TP | ||
48 | \fB\-\-no\-manifest\-update\fR, \fB\-\-nmu\fR | ||
49 | use the existing manifest checkout as\-is. (do not | ||
50 | update to the latest revision) | ||
51 | .TP | ||
52 | \fB\-n\fR, \fB\-\-network\-only\fR | ||
53 | fetch only, don't update working tree | ||
54 | .TP | ||
55 | \fB\-d\fR, \fB\-\-detach\fR | ||
56 | detach projects back to manifest revision | ||
57 | .TP | ||
58 | \fB\-c\fR, \fB\-\-current\-branch\fR | ||
59 | fetch only current branch from server | ||
60 | .TP | ||
61 | \fB\-\-no\-current\-branch\fR | ||
62 | fetch all branches from server | ||
63 | .TP | ||
64 | \fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml | ||
65 | temporary manifest to use for this sync | ||
66 | .TP | ||
67 | \fB\-\-clone\-bundle\fR | ||
68 | enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS | ||
69 | .TP | ||
70 | \fB\-\-no\-clone\-bundle\fR | ||
71 | disable 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 | ||
74 | username 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 | ||
77 | password to authenticate with the manifest server | ||
78 | .TP | ||
79 | \fB\-\-fetch\-submodules\fR | ||
80 | fetch submodules from server | ||
81 | .TP | ||
82 | \fB\-\-use\-superproject\fR | ||
83 | use the manifest superproject to sync projects | ||
84 | .TP | ||
85 | \fB\-\-no\-use\-superproject\fR | ||
86 | disable use of manifest superprojects | ||
87 | .TP | ||
88 | \fB\-\-tags\fR | ||
89 | fetch tags | ||
90 | .TP | ||
91 | \fB\-\-no\-tags\fR | ||
92 | don't fetch tags | ||
93 | .TP | ||
94 | \fB\-\-optimized\-fetch\fR | ||
95 | only fetch projects fixed to sha1 if revision does not | ||
96 | exist locally | ||
97 | .TP | ||
98 | \fB\-\-retry\-fetches\fR=\fI\,RETRY_FETCHES\/\fR | ||
99 | number of times to retry fetches on transient errors | ||
100 | .TP | ||
101 | \fB\-\-prune\fR | ||
102 | delete refs that no longer exist on the remote | ||
103 | .SS Logging options: | ||
104 | .TP | ||
105 | \fB\-v\fR, \fB\-\-verbose\fR | ||
106 | show all output | ||
107 | .TP | ||
108 | \fB\-q\fR, \fB\-\-quiet\fR | ||
109 | only show errors | ||
110 | .SS repo Version options: | ||
111 | .TP | ||
112 | \fB\-\-no\-repo\-verify\fR | ||
113 | do not verify repo source code | ||
114 | .PP | ||
115 | Run `repo help smartsync` to view the detailed manual. | ||
116 | .SH DETAILS | ||
117 | .PP | ||
118 | The '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 | ||
4 | repo \- repo stage - manual page for repo stage | ||
5 | .SH SYNOPSIS | ||
6 | .B repo | ||
7 | \fI\,stage -i \/\fR[\fI\,<project>\/\fR...] | ||
8 | .SH DESCRIPTION | ||
9 | Summary | ||
10 | .PP | ||
11 | Stage file(s) for commit | ||
12 | .SH OPTIONS | ||
13 | .TP | ||
14 | \fB\-h\fR, \fB\-\-help\fR | ||
15 | show this help message and exit | ||
16 | .SS Logging options: | ||
17 | .TP | ||
18 | \fB\-v\fR, \fB\-\-verbose\fR | ||
19 | show all output | ||
20 | .TP | ||
21 | \fB\-q\fR, \fB\-\-quiet\fR | ||
22 | only show errors | ||
23 | .TP | ||
24 | \fB\-i\fR, \fB\-\-interactive\fR | ||
25 | use interactive staging | ||
26 | .PP | ||
27 | Run `repo help stage` to view the detailed manual. | ||
28 | .SH DETAILS | ||
29 | .PP | ||
30 | The '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 | ||
4 | repo \- 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 | ||
9 | Summary | ||
10 | .PP | ||
11 | Start a new branch for development | ||
12 | .SH OPTIONS | ||
13 | .TP | ||
14 | \fB\-h\fR, \fB\-\-help\fR | ||
15 | show this help message and exit | ||
16 | .TP | ||
17 | \fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR | ||
18 | number of jobs to run in parallel (default: based on | ||
19 | number of CPU cores) | ||
20 | .TP | ||
21 | \fB\-\-all\fR | ||
22 | begin branch in all projects | ||
23 | .TP | ||
24 | \fB\-r\fR REVISION, \fB\-\-rev\fR=\fI\,REVISION\/\fR, \fB\-\-revision\fR=\fI\,REVISION\/\fR | ||
25 | point branch at this revision instead of upstream | ||
26 | .TP | ||
27 | \fB\-\-head\fR, \fB\-\-HEAD\fR | ||
28 | abbreviation for \fB\-\-rev\fR HEAD | ||
29 | .SS Logging options: | ||
30 | .TP | ||
31 | \fB\-v\fR, \fB\-\-verbose\fR | ||
32 | show all output | ||
33 | .TP | ||
34 | \fB\-q\fR, \fB\-\-quiet\fR | ||
35 | only show errors | ||
36 | .PP | ||
37 | Run `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 | ||
41 | specified 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 | ||
4 | repo \- repo status - manual page for repo status | ||
5 | .SH SYNOPSIS | ||
6 | .B repo | ||
7 | \fI\,status \/\fR[\fI\,<project>\/\fR...] | ||
8 | .SH DESCRIPTION | ||
9 | Summary | ||
10 | .PP | ||
11 | Show the working tree status | ||
12 | .SH OPTIONS | ||
13 | .TP | ||
14 | \fB\-h\fR, \fB\-\-help\fR | ||
15 | show this help message and exit | ||
16 | .TP | ||
17 | \fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR | ||
18 | number of jobs to run in parallel (default: based on | ||
19 | number of CPU cores) | ||
20 | .TP | ||
21 | \fB\-o\fR, \fB\-\-orphans\fR | ||
22 | include objects in working directory outside of repo | ||
23 | projects | ||
24 | .SS Logging options: | ||
25 | .TP | ||
26 | \fB\-v\fR, \fB\-\-verbose\fR | ||
27 | show all output | ||
28 | .TP | ||
29 | \fB\-q\fR, \fB\-\-quiet\fR | ||
30 | only show errors | ||
31 | .PP | ||
32 | Run `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 | ||
36 | most recent commit on this branch (HEAD), in each project specified. A summary | ||
37 | is displayed, one line per file where there is a difference between these three | ||
38 | states. | ||
39 | .PP | ||
40 | The \fB\-j\fR/\-\-jobs option can be used to run multiple status queries in parallel. | ||
41 | .PP | ||
42 | The \fB\-o\fR/\-\-orphans option can be used to show objects that are in the working | ||
43 | directory, but not associated with a repo project. This includes unmanaged | ||
44 | top\-level files and directories, but also includes deeper items. For example, if | ||
45 | dir/subdir/proj1 and dir/subdir/proj2 are repo projects, dir/subdir/proj3 will | ||
46 | be shown if it is not known to repo. | ||
47 | .PP | ||
48 | Status Display | ||
49 | .PP | ||
50 | The status display is organized into three columns of information, for example | ||
51 | if the file 'subcmds/status.py' is modified in the project 'repo' on branch | ||
52 | \&'devwork': | ||
53 | .TP | ||
54 | project repo/ | ||
55 | branch devwork | ||
56 | .TP | ||
57 | \fB\-m\fR | ||
58 | subcmds/status.py | ||
59 | .PP | ||
60 | The first column explains how the staging area (index) differs from the last | ||
61 | commit (HEAD). Its values are always displayed in upper case and have the | ||
62 | following meanings: | ||
63 | .TP | ||
64 | \-: | ||
65 | no difference | ||
66 | .TP | ||
67 | A: | ||
68 | added (not in HEAD, in index ) | ||
69 | .TP | ||
70 | M: | ||
71 | modified ( in HEAD, in index, different content ) | ||
72 | .TP | ||
73 | D: | ||
74 | deleted ( in HEAD, not in index ) | ||
75 | .TP | ||
76 | R: | ||
77 | renamed (not in HEAD, in index, path changed ) | ||
78 | .TP | ||
79 | C: | ||
80 | copied (not in HEAD, in index, copied from another) | ||
81 | .TP | ||
82 | T: | ||
83 | mode changed ( in HEAD, in index, same content ) | ||
84 | .TP | ||
85 | U: | ||
86 | unmerged; conflict resolution required | ||
87 | .PP | ||
88 | The second column explains how the working directory differs from the index. Its | ||
89 | values are always displayed in lower case and have the following meanings: | ||
90 | .TP | ||
91 | \-: | ||
92 | new / unknown (not in index, in work tree ) | ||
93 | .TP | ||
94 | m: | ||
95 | modified ( in index, in work tree, modified ) | ||
96 | .TP | ||
97 | d: | ||
98 | deleted ( 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 | ||
4 | repo \- repo sync - manual page for repo sync | ||
5 | .SH SYNOPSIS | ||
6 | .B repo | ||
7 | \fI\,sync \/\fR[\fI\,<project>\/\fR...] | ||
8 | .SH DESCRIPTION | ||
9 | Summary | ||
10 | .PP | ||
11 | Update working tree to the latest revision | ||
12 | .SH OPTIONS | ||
13 | .TP | ||
14 | \fB\-h\fR, \fB\-\-help\fR | ||
15 | show this help message and exit | ||
16 | .TP | ||
17 | \fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR | ||
18 | number of jobs to run in parallel (default: based on | ||
19 | number of CPU cores) | ||
20 | .TP | ||
21 | \fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR | ||
22 | number of network jobs to run in parallel (defaults to | ||
23 | \fB\-\-jobs\fR) | ||
24 | .TP | ||
25 | \fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR | ||
26 | number of local checkout jobs to run in parallel | ||
27 | (defaults to \fB\-\-jobs\fR) | ||
28 | .TP | ||
29 | \fB\-f\fR, \fB\-\-force\-broken\fR | ||
30 | obsolete option (to be deleted in the future) | ||
31 | .TP | ||
32 | \fB\-\-fail\-fast\fR | ||
33 | stop syncing after first error is hit | ||
34 | .TP | ||
35 | \fB\-\-force\-sync\fR | ||
36 | overwrite an existing git directory if it needs to | ||
37 | point to a different object directory. WARNING: this | ||
38 | may cause loss of data | ||
39 | .TP | ||
40 | \fB\-\-force\-remove\-dirty\fR | ||
41 | force remove projects with uncommitted modifications | ||
42 | if projects no longer exist in the manifest. WARNING: | ||
43 | this may cause loss of data | ||
44 | .TP | ||
45 | \fB\-l\fR, \fB\-\-local\-only\fR | ||
46 | only update working tree, don't fetch | ||
47 | .TP | ||
48 | \fB\-\-no\-manifest\-update\fR, \fB\-\-nmu\fR | ||
49 | use the existing manifest checkout as\-is. (do not | ||
50 | update to the latest revision) | ||
51 | .TP | ||
52 | \fB\-n\fR, \fB\-\-network\-only\fR | ||
53 | fetch only, don't update working tree | ||
54 | .TP | ||
55 | \fB\-d\fR, \fB\-\-detach\fR | ||
56 | detach projects back to manifest revision | ||
57 | .TP | ||
58 | \fB\-c\fR, \fB\-\-current\-branch\fR | ||
59 | fetch only current branch from server | ||
60 | .TP | ||
61 | \fB\-\-no\-current\-branch\fR | ||
62 | fetch all branches from server | ||
63 | .TP | ||
64 | \fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml | ||
65 | temporary manifest to use for this sync | ||
66 | .TP | ||
67 | \fB\-\-clone\-bundle\fR | ||
68 | enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS | ||
69 | .TP | ||
70 | \fB\-\-no\-clone\-bundle\fR | ||
71 | disable 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 | ||
74 | username 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 | ||
77 | password to authenticate with the manifest server | ||
78 | .TP | ||
79 | \fB\-\-fetch\-submodules\fR | ||
80 | fetch submodules from server | ||
81 | .TP | ||
82 | \fB\-\-use\-superproject\fR | ||
83 | use the manifest superproject to sync projects | ||
84 | .TP | ||
85 | \fB\-\-no\-use\-superproject\fR | ||
86 | disable use of manifest superprojects | ||
87 | .TP | ||
88 | \fB\-\-tags\fR | ||
89 | fetch tags | ||
90 | .TP | ||
91 | \fB\-\-no\-tags\fR | ||
92 | don't fetch tags | ||
93 | .TP | ||
94 | \fB\-\-optimized\-fetch\fR | ||
95 | only fetch projects fixed to sha1 if revision does not | ||
96 | exist locally | ||
97 | .TP | ||
98 | \fB\-\-retry\-fetches\fR=\fI\,RETRY_FETCHES\/\fR | ||
99 | number of times to retry fetches on transient errors | ||
100 | .TP | ||
101 | \fB\-\-prune\fR | ||
102 | delete refs that no longer exist on the remote | ||
103 | .TP | ||
104 | \fB\-s\fR, \fB\-\-smart\-sync\fR | ||
105 | smart sync using manifest from the latest known good | ||
106 | build | ||
107 | .TP | ||
108 | \fB\-t\fR SMART_TAG, \fB\-\-smart\-tag\fR=\fI\,SMART_TAG\/\fR | ||
109 | smart sync using manifest from a known tag | ||
110 | .SS Logging options: | ||
111 | .TP | ||
112 | \fB\-v\fR, \fB\-\-verbose\fR | ||
113 | show all output | ||
114 | .TP | ||
115 | \fB\-q\fR, \fB\-\-quiet\fR | ||
116 | only show errors | ||
117 | .SS repo Version options: | ||
118 | .TP | ||
119 | \fB\-\-no\-repo\-verify\fR | ||
120 | do not verify repo source code | ||
121 | .PP | ||
122 | Run `repo help sync` to view the detailed manual. | ||
123 | .SH DETAILS | ||
124 | .PP | ||
125 | The 'repo sync' command synchronizes local project directories with the remote | ||
126 | repositories specified in the manifest. If a local project does not yet exist, | ||
127 | it will clone a new local directory from the remote repository and set up | ||
128 | tracking branches as specified in the manifest. If the local project already | ||
129 | exists, 'repo sync' will update the remote branches and rebase any new local | ||
130 | changes on top of the new remote changes. | ||
131 | .PP | ||
132 | \&'repo sync' will synchronize all projects listed at the command line. Projects | ||
133 | can be specified either by name, or by a relative or absolute path to the | ||
134 | project's local directory. If no projects are specified, 'repo sync' will | ||
135 | synchronize all projects listed in the manifest. | ||
136 | .PP | ||
137 | The \fB\-d\fR/\-\-detach option can be used to switch specified projects back to the | ||
138 | manifest revision. This option is especially helpful if the project is currently | ||
139 | on a topic branch, but the manifest revision is temporarily needed. | ||
140 | .PP | ||
141 | The \fB\-s\fR/\-\-smart\-sync option can be used to sync to a known good build as | ||
142 | specified 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 | ||
145 | The \fB\-u\fR/\-\-manifest\-server\-username and \fB\-p\fR/\-\-manifest\-server\-password options can | ||
146 | be used to specify a username and password to authenticate with the manifest | ||
147 | server when using the \fB\-s\fR or \fB\-t\fR option. | ||
148 | .PP | ||
149 | If \fB\-u\fR and \fB\-p\fR are not specified when using the \fB\-s\fR or \fB\-t\fR option, 'repo sync' will | ||
150 | attempt to read authentication credentials for the manifest server from the | ||
151 | user's .netrc file. | ||
152 | .PP | ||
153 | \&'repo sync' will not use authentication credentials from \fB\-u\fR/\-p or .netrc if the | ||
154 | manifest server specified in the manifest file already includes credentials. | ||
155 | .PP | ||
156 | By default, all projects will be synced. The \fB\-\-fail\-fast\fR option can be used to | ||
157 | halt syncing as soon as possible when the first project fails to sync. | ||
158 | .PP | ||
159 | The \fB\-\-force\-sync\fR option can be used to overwrite existing git directories if | ||
160 | they have previously been linked to a different object directory. WARNING: This | ||
161 | may cause data to be lost since refs may be removed when overwriting. | ||
162 | .PP | ||
163 | The \fB\-\-force\-remove\-dirty\fR option can be used to remove previously used projects | ||
164 | with uncommitted changes. WARNING: This may cause data to be lost since | ||
165 | uncommitted changes may be removed with projects that no longer exist in the | ||
166 | manifest. | ||
167 | .PP | ||
168 | The \fB\-\-no\-clone\-bundle\fR option disables any attempt to use \fI\,$URL/clone.bundle\/\fP to | ||
169 | bootstrap a new Git repository from a resumeable bundle file on a content | ||
170 | delivery network. This may be necessary if there are problems with the local | ||
171 | Python HTTP client or proxy configuration, but the Git binary works. | ||
172 | .PP | ||
173 | The \fB\-\-fetch\-submodules\fR option enables fetching Git submodules of a project from | ||
174 | server. | ||
175 | .PP | ||
176 | The \fB\-c\fR/\-\-current\-branch option can be used to only fetch objects that are on the | ||
177 | branch specified by a project's revision. | ||
178 | .PP | ||
179 | The \fB\-\-optimized\-fetch\fR option can be used to only fetch projects that are fixed | ||
180 | to a sha1 revision if the sha1 revision does not already exist locally. | ||
181 | .PP | ||
182 | The \fB\-\-prune\fR option can be used to remove any refs that no longer exist on the | ||
183 | remote. | ||
184 | .PP | ||
185 | SSH Connections | ||
186 | .PP | ||
187 | If at least one project remote URL uses an SSH connection (ssh://, git+ssh://, | ||
188 | or user@host:path syntax) repo will automatically enable the SSH ControlMaster | ||
189 | option when connecting to that host. This feature permits other projects in the | ||
190 | same 'repo sync' session to reuse the same SSH tunnel, saving connection setup | ||
191 | overheads. | ||
192 | .PP | ||
193 | To disable this behavior on UNIX platforms, set the GIT_SSH environment variable | ||
194 | to 'ssh'. For example: | ||
195 | .IP | ||
196 | export GIT_SSH=ssh | ||
197 | repo sync | ||
198 | .PP | ||
199 | Compatibility | ||
200 | .PP | ||
201 | This feature is automatically disabled on Windows, due to the lack of UNIX | ||
202 | domain socket support. | ||
203 | .PP | ||
204 | This 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 | ||
206 | enough to establish the ControlMaster tunnel. | ||
207 | .PP | ||
208 | If the remote SSH daemon is Gerrit Code Review, version 2.0.10 or later is | ||
209 | required 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 | ||
4 | repo \- 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 | ||
9 | Summary | ||
10 | .PP | ||
11 | Upload changes for code review | ||
12 | .SH OPTIONS | ||
13 | .TP | ||
14 | \fB\-h\fR, \fB\-\-help\fR | ||
15 | show this help message and exit | ||
16 | .TP | ||
17 | \fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR | ||
18 | number of jobs to run in parallel (default: based on | ||
19 | number of CPU cores) | ||
20 | .TP | ||
21 | \fB\-t\fR | ||
22 | send local branch name to Gerrit Code Review | ||
23 | .TP | ||
24 | \fB\-\-hashtag\fR=\fI\,HASHTAGS\/\fR, \fB\-\-ht\fR=\fI\,HASHTAGS\/\fR | ||
25 | add hashtags (comma delimited) to the review | ||
26 | .TP | ||
27 | \fB\-\-hashtag\-branch\fR, \fB\-\-htb\fR | ||
28 | add local branch name as a hashtag | ||
29 | .TP | ||
30 | \fB\-l\fR LABELS, \fB\-\-label\fR=\fI\,LABELS\/\fR | ||
31 | add a label when uploading | ||
32 | .TP | ||
33 | \fB\-\-re\fR=\fI\,REVIEWERS\/\fR, \fB\-\-reviewers\fR=\fI\,REVIEWERS\/\fR | ||
34 | request reviews from these people | ||
35 | .TP | ||
36 | \fB\-\-cc\fR=\fI\,CC\/\fR | ||
37 | also 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 | ||
43 | upload current git branch | ||
44 | .TP | ||
45 | \fB\-\-no\-current\-branch\fR | ||
46 | upload all git branches | ||
47 | .TP | ||
48 | \fB\-\-ne\fR, \fB\-\-no\-emails\fR | ||
49 | do not send e\-mails on upload | ||
50 | .TP | ||
51 | \fB\-p\fR, \fB\-\-private\fR | ||
52 | upload as a private change (deprecated; use \fB\-\-wip\fR) | ||
53 | .TP | ||
54 | \fB\-w\fR, \fB\-\-wip\fR | ||
55 | upload as a work\-in\-progress change | ||
56 | .TP | ||
57 | \fB\-o\fR PUSH_OPTIONS, \fB\-\-push\-option\fR=\fI\,PUSH_OPTIONS\/\fR | ||
58 | additional push options to transmit | ||
59 | .TP | ||
60 | \fB\-D\fR BRANCH, \fB\-\-destination\fR=\fI\,BRANCH\/\fR, \fB\-\-dest\fR=\fI\,BRANCH\/\fR | ||
61 | submit for review on this target branch | ||
62 | .TP | ||
63 | \fB\-n\fR, \fB\-\-dry\-run\fR | ||
64 | do everything except actually upload the CL | ||
65 | .TP | ||
66 | \fB\-y\fR, \fB\-\-yes\fR | ||
67 | answer yes to all safe prompts | ||
68 | .TP | ||
69 | \fB\-\-no\-cert\-checks\fR | ||
70 | disable verifying ssl certs (unsafe) | ||
71 | .SS Logging options: | ||
72 | .TP | ||
73 | \fB\-v\fR, \fB\-\-verbose\fR | ||
74 | show all output | ||
75 | .TP | ||
76 | \fB\-q\fR, \fB\-\-quiet\fR | ||
77 | only show errors | ||
78 | .SS pre\-upload hooks: | ||
79 | .TP | ||
80 | \fB\-\-no\-verify\fR | ||
81 | Do not run the pre\-upload hook. | ||
82 | .TP | ||
83 | \fB\-\-verify\fR | ||
84 | Run the pre\-upload hook without prompting. | ||
85 | .TP | ||
86 | \fB\-\-ignore\-hooks\fR | ||
87 | Do not abort if pre\-upload hooks fail. | ||
88 | .PP | ||
89 | Run `repo help upload` to view the detailed manual. | ||
90 | .SH DETAILS | ||
91 | .PP | ||
92 | The 'repo upload' command is used to send changes to the Gerrit Code Review | ||
93 | system. It searches for topic branches in local projects that have not yet been | ||
94 | published for review. If multiple topic branches are found, 'repo upload' opens | ||
95 | an 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 | ||
98 | command line. Projects can be specified either by name, or by a relative or | ||
99 | absolute 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 | ||
101 | manifest. | ||
102 | .PP | ||
103 | If the \fB\-\-reviewers\fR or \fB\-\-cc\fR options are passed, those emails are added to the | ||
104 | respective 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 | ||
106 | upload will fail. | ||
107 | .PP | ||
108 | Configuration | ||
109 | .PP | ||
110 | review.URL.autoupload: | ||
111 | .PP | ||
112 | To disable the "Upload ... (y/N)?" prompt, you can set a per\-project or global | ||
113 | Git configuration option. If review.URL.autoupload is set to "true" then repo | ||
114 | will assume you always answer "y" at the prompt, and will not prompt you | ||
115 | further. If it is set to "false" then repo will assume you always answer "n", | ||
116 | and will abort. | ||
117 | .PP | ||
118 | review.URL.autoreviewer: | ||
119 | .PP | ||
120 | To automatically append a user or mailing list to reviews, you can set a | ||
121 | per\-project or global Git option to do so. | ||
122 | .PP | ||
123 | review.URL.autocopy: | ||
124 | .PP | ||
125 | To automatically copy a user or mailing list to all uploaded reviews, you can | ||
126 | set a per\-project or global Git option to do so. Specifically, | ||
127 | review.URL.autocopy can be set to a comma separated list of reviewers who you | ||
128 | always want copied on all uploads with a non\-empty \fB\-\-re\fR argument. | ||
129 | .PP | ||
130 | review.URL.username: | ||
131 | .PP | ||
132 | Override the username used to connect to Gerrit Code Review. By default the | ||
133 | local part of the email address is used. | ||
134 | .PP | ||
135 | The 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 | ||
140 | url = git://git.example.com/project.git | ||
141 | review = http://review.example.com/ | ||
142 | .IP | ||
143 | [review "http://review.example.com/"] | ||
144 | .IP | ||
145 | autoupload = true | ||
146 | autocopy = johndoe@company.com,my\-team\-alias@company.com | ||
147 | .PP | ||
148 | review.URL.uploadtopic: | ||
149 | .PP | ||
150 | To add a topic branch whenever uploading a commit, you can set a per\-project or | ||
151 | global Git option to do so. If review.URL.uploadtopic is set to "true" then repo | ||
152 | will assume you always want the equivalent of the \fB\-t\fR option to the repo command. | ||
153 | If unset or set to "false" then repo will make use of only the command line | ||
154 | option. | ||
155 | .PP | ||
156 | review.URL.uploadhashtags: | ||
157 | .PP | ||
158 | To add hashtags whenever uploading a commit, you can set a per\-project or global | ||
159 | Git option to do so. The value of review.URL.uploadhashtags will be used as | ||
160 | comma delimited hashtags like the \fB\-\-hashtag\fR option. | ||
161 | .PP | ||
162 | review.URL.uploadlabels: | ||
163 | .PP | ||
164 | To add labels whenever uploading a commit, you can set a per\-project or global | ||
165 | Git option to do so. The value of review.URL.uploadlabels will be used as comma | ||
166 | delimited labels like the \fB\-\-label\fR option. | ||
167 | .PP | ||
168 | review.URL.uploadnotify: | ||
169 | .PP | ||
170 | Control e\-mail notifications when uploading. | ||
171 | https://gerrit\-review.googlesource.com/Documentation/user\-upload.html#notify | ||
172 | .PP | ||
173 | References | ||
174 | .PP | ||
175 | Gerrit 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 | ||
4 | repo \- repo version - manual page for repo version | ||
5 | .SH SYNOPSIS | ||
6 | .B repo | ||
7 | \fI\,version\/\fR | ||
8 | .SH DESCRIPTION | ||
9 | Summary | ||
10 | .PP | ||
11 | Display the version of repo | ||
12 | .SH OPTIONS | ||
13 | .TP | ||
14 | \fB\-h\fR, \fB\-\-help\fR | ||
15 | show this help message and exit | ||
16 | .SS Logging options: | ||
17 | .TP | ||
18 | \fB\-v\fR, \fB\-\-verbose\fR | ||
19 | show all output | ||
20 | .TP | ||
21 | \fB\-q\fR, \fB\-\-quiet\fR | ||
22 | only show errors | ||
23 | .PP | ||
24 | Run `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 | ||
4 | repo \- repository management tool built on top of git | ||
5 | .SH SYNOPSIS | ||
6 | .B repo | ||
7 | [\fI\,-p|--paginate|--no-pager\/\fR] \fI\,COMMAND \/\fR[\fI\,ARGS\/\fR] | ||
8 | .SH OPTIONS | ||
9 | .TP | ||
10 | \fB\-h\fR, \fB\-\-help\fR | ||
11 | show this help message and exit | ||
12 | .TP | ||
13 | \fB\-\-help\-all\fR | ||
14 | show this help message with all subcommands and exit | ||
15 | .TP | ||
16 | \fB\-p\fR, \fB\-\-paginate\fR | ||
17 | display command output in the pager | ||
18 | .TP | ||
19 | \fB\-\-no\-pager\fR | ||
20 | disable the pager | ||
21 | .TP | ||
22 | \fB\-\-color\fR=\fI\,COLOR\/\fR | ||
23 | control color usage: auto, always, never | ||
24 | .TP | ||
25 | \fB\-\-trace\fR | ||
26 | trace git command execution (REPO_TRACE=1) | ||
27 | .TP | ||
28 | \fB\-\-trace\-python\fR | ||
29 | trace python command execution | ||
30 | .TP | ||
31 | \fB\-\-time\fR | ||
32 | time repo command execution | ||
33 | .TP | ||
34 | \fB\-\-version\fR | ||
35 | display this version of repo | ||
36 | .TP | ||
37 | \fB\-\-show\-toplevel\fR | ||
38 | display the path of the top\-level directory of the | ||
39 | repo client checkout | ||
40 | .TP | ||
41 | \fB\-\-event\-log\fR=\fI\,EVENT_LOG\/\fR | ||
42 | filename of event log to append timeline to | ||
43 | .TP | ||
44 | \fB\-\-git\-trace2\-event\-log\fR=\fI\,GIT_TRACE2_EVENT_LOG\/\fR | ||
45 | directory to write git trace2 event log to | ||
46 | .SS "The complete list of recognized repo commands are:" | ||
47 | .TP | ||
48 | abandon | ||
49 | Permanently abandon a development branch | ||
50 | .TP | ||
51 | branch | ||
52 | View current topic branches | ||
53 | .TP | ||
54 | branches | ||
55 | View current topic branches | ||
56 | .TP | ||
57 | checkout | ||
58 | Checkout a branch for development | ||
59 | .TP | ||
60 | cherry\-pick | ||
61 | Cherry\-pick a change. | ||
62 | .TP | ||
63 | diff | ||
64 | Show changes between commit and working tree | ||
65 | .TP | ||
66 | diffmanifests | ||
67 | Manifest diff utility | ||
68 | .TP | ||
69 | download | ||
70 | Download and checkout a change | ||
71 | .TP | ||
72 | forall | ||
73 | Run a shell command in each project | ||
74 | .TP | ||
75 | gitc\-delete | ||
76 | Delete a GITC Client. | ||
77 | .TP | ||
78 | gitc\-init | ||
79 | Initialize a GITC Client. | ||
80 | .TP | ||
81 | grep | ||
82 | Print lines matching a pattern | ||
83 | .TP | ||
84 | help | ||
85 | Display detailed help on a command | ||
86 | .TP | ||
87 | info | ||
88 | Get info on the manifest branch, current branch or unmerged branches | ||
89 | .TP | ||
90 | init | ||
91 | Initialize a repo client checkout in the current directory | ||
92 | .TP | ||
93 | list | ||
94 | List projects and their associated directories | ||
95 | .TP | ||
96 | manifest | ||
97 | Manifest inspection utility | ||
98 | .TP | ||
99 | overview | ||
100 | Display overview of unmerged project branches | ||
101 | .TP | ||
102 | prune | ||
103 | Prune (delete) already merged topics | ||
104 | .TP | ||
105 | rebase | ||
106 | Rebase local branches on upstream branch | ||
107 | .TP | ||
108 | selfupdate | ||
109 | Update repo to the latest version | ||
110 | .TP | ||
111 | smartsync | ||
112 | Update working tree to the latest known good revision | ||
113 | .TP | ||
114 | stage | ||
115 | Stage file(s) for commit | ||
116 | .TP | ||
117 | start | ||
118 | Start a new branch for development | ||
119 | .TP | ||
120 | status | ||
121 | Show the working tree status | ||
122 | .TP | ||
123 | sync | ||
124 | Update working tree to the latest revision | ||
125 | .TP | ||
126 | upload | ||
127 | Upload changes for code review | ||
128 | .TP | ||
129 | version | ||
130 | Display the version of repo | ||
131 | .PP | ||
132 | See 'repo help <command>' for more information on a specific command. | ||
133 | Bug 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 | ||
17 | from __future__ import print_function | 15 | import collections |
18 | import itertools | 16 | import itertools |
19 | import os | 17 | import os |
18 | import platform | ||
20 | import re | 19 | import re |
21 | import sys | 20 | import sys |
22 | import xml.dom.minidom | 21 | import xml.dom.minidom |
23 | 22 | import urllib.parse | |
24 | from pyversion import is_python3 | ||
25 | if is_python3(): | ||
26 | import urllib.parse | ||
27 | else: | ||
28 | import imp | ||
29 | import urlparse | ||
30 | urllib = imp.new_module('urllib') | ||
31 | urllib.parse = urlparse | ||
32 | 23 | ||
33 | import gitc_utils | 24 | import gitc_utils |
34 | from git_config import GitConfig | 25 | from git_config import GitConfig, IsId |
35 | from git_refs import R_HEADS, HEAD | 26 | from git_refs import R_HEADS, HEAD |
36 | import platform_utils | 27 | import platform_utils |
37 | from project import RemoteSpec, Project, MetaProject | 28 | from project import Annotation, RemoteSpec, Project, MetaProject |
38 | from error import ManifestParseError, ManifestInvalidRevisionError | 29 | from error import (ManifestParseError, ManifestInvalidPathError, |
30 | ManifestInvalidRevisionError) | ||
31 | from wrapper import Wrapper | ||
39 | 32 | ||
40 | MANIFEST_FILE_NAME = 'manifest.xml' | 33 | MANIFEST_FILE_NAME = 'manifest.xml' |
41 | LOCAL_MANIFEST_NAME = 'local_manifest.xml' | 34 | LOCAL_MANIFEST_NAME = 'local_manifest.xml' |
42 | LOCAL_MANIFESTS_DIR_NAME = 'local_manifests' | 35 | LOCAL_MANIFESTS_DIR_NAME = 'local_manifests' |
43 | 36 | ||
37 | # Add all projects from local manifest into a group. | ||
38 | LOCAL_MANIFEST_GROUP_PREFIX = 'local:' | ||
39 | |||
40 | # ContactInfo has the self-registered bug url, supplied by the manifest authors. | ||
41 | ContactInfo = 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. |
45 | urllib.parse.uses_relative.extend([ | 44 | urllib.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 | |||
58 | def 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 | |||
87 | def 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 | |||
58 | class _Default(object): | 112 | class _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 | |||
76 | class _XmlRemote(object): | 135 | class _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 | |||
129 | class XmlManifest(object): | 200 | class 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 | <!-- | ||
279 | DO NOT EDIT THIS FILE! It is generated by repo and changes will be discarded. | ||
280 | If you want to use a different manifest, use `repo init -m <file>` instead. | ||
281 | |||
282 | If you want to customize your checkout by overriding manifest settings, use | ||
283 | the local_manifests/ directory instead. | ||
284 | |||
285 | For more information on repo manifests, check out: | ||
286 | https://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 | ||
1031 | class GitcManifest(XmlManifest): | 1474 | class 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 | |||
1488 | class 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 | |||
1509 | class 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 | ||
@@ -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 | ||
17 | from __future__ import print_function | ||
18 | import os | 15 | import os |
19 | import select | 16 | import select |
20 | import subprocess | 17 | import subprocess |
@@ -27,6 +24,7 @@ pager_process = None | |||
27 | old_stdout = None | 24 | old_stdout = None |
28 | old_stderr = None | 25 | old_stderr = None |
29 | 26 | ||
27 | |||
30 | def RunPager(globalConfig): | 28 | def 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 | |||
42 | def TerminatePager(): | 41 | def 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 | |||
55 | def _PipePager(pager): | 55 | def _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 | |||
65 | def _ForkPager(pager): | 67 | def _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 | |||
91 | def _SelectPager(globalConfig): | 94 | def _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 | |||
108 | def _BecomePager(pager): | 112 | def _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 @@ | |||
17 | import errno | 15 | import errno |
18 | import os | 16 | import os |
19 | import platform | 17 | import platform |
20 | import select | ||
21 | import shutil | 18 | import shutil |
22 | import stat | 19 | import stat |
23 | 20 | ||
24 | from pyversion import is_python3 | ||
25 | if is_python3(): | ||
26 | from queue import Queue | ||
27 | else: | ||
28 | from Queue import Queue | ||
29 | |||
30 | from threading import Thread | ||
31 | |||
32 | 21 | ||
33 | def isWindows(): | 22 | def 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 | ||
42 | class 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 | |||
89 | class _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 | |||
123 | class _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 | |||
181 | def symlink(source, link_name): | 31 | def 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 | ||
280 | def remove(path): | 130 | def 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 | ||
304 | def walk(top, topdown=True, onerror=None, followlinks=False): | 153 | def 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 | ||
17 | import errno | 15 | import errno |
18 | 16 | ||
19 | from pyversion import is_python3 | ||
20 | from ctypes import WinDLL, get_last_error, FormatError, WinError, addressof | 17 | from ctypes import WinDLL, get_last_error, FormatError, WinError, addressof |
21 | from ctypes import c_buffer | 18 | from ctypes import c_buffer, c_ubyte, Structure, Union, byref |
22 | from ctypes.wintypes import BOOL, BOOLEAN, LPCWSTR, DWORD, HANDLE, POINTER, c_ubyte | 19 | from ctypes.wintypes import BOOL, BOOLEAN, LPCWSTR, DWORD, HANDLE |
23 | from ctypes.wintypes import WCHAR, USHORT, LPVOID, Structure, Union, ULONG | 20 | from ctypes.wintypes import WCHAR, USHORT, LPVOID, ULONG, LPDWORD |
24 | from ctypes.wintypes import byref | ||
25 | 21 | ||
26 | kernel32 = WinDLL('kernel32', use_last_error=True) | 22 | kernel32 = WinDLL('kernel32', use_last_error=True) |
27 | 23 | ||
28 | LPDWORD = POINTER(DWORD) | ||
29 | UCHAR = c_ubyte | 24 | UCHAR = 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 | ||
149 | def _create_symlink(source, link_name, dwFlags): | 144 | def _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 | ||
210 | def _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 | |||
221 | def _raise_winerror(code, error_desc): | 206 | def _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. |
27 | CSI_ERASE_LINE = '\x1b[2K' | 25 | CSI_ERASE_LINE = '\x1b[2K' |
28 | 26 | ||
27 | |||
28 | def 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 | |||
29 | class Progress(object): | 44 | class 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() |
@@ -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 | ||
17 | from __future__ import print_function | ||
18 | import errno | 15 | import errno |
19 | import filecmp | 16 | import filecmp |
20 | import glob | 17 | import glob |
21 | import json | ||
22 | import os | 18 | import os |
23 | import random | 19 | import random |
24 | import re | 20 | import re |
@@ -29,36 +25,33 @@ import sys | |||
29 | import tarfile | 25 | import tarfile |
30 | import tempfile | 26 | import tempfile |
31 | import time | 27 | import time |
32 | import traceback | 28 | import urllib.parse |
33 | 29 | ||
34 | from color import Coloring | 30 | from color import Coloring |
35 | from git_command import GitCommand, git_require | 31 | from git_command import GitCommand, git_require |
36 | from git_config import GitConfig, IsId, GetSchemeFromUrl, GetUrlCookieFile, \ | 32 | from git_config import GitConfig, IsId, GetSchemeFromUrl, GetUrlCookieFile, \ |
37 | ID_RE | 33 | ID_RE |
38 | from error import GitError, HookError, UploadError, DownloadError | 34 | from error import GitError, UploadError, DownloadError |
39 | from error import ManifestInvalidRevisionError | 35 | from error import ManifestInvalidRevisionError, ManifestInvalidPathError |
40 | from error import NoManifestException | 36 | from error import NoManifestException |
41 | import platform_utils | 37 | import platform_utils |
42 | import progress | 38 | import progress |
43 | from repo_trace import IsTrace, Trace | 39 | from repo_trace import IsTrace, Trace |
44 | 40 | ||
45 | from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M | 41 | from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M, R_WORKTREE_M |
42 | |||
46 | 43 | ||
47 | from pyversion import is_python3 | 44 | # Maximum sleep time allowed during retries. |
48 | if is_python3(): | 45 | MAXIMUM_RETRY_SLEEP_SEC = 3600.0 |
49 | import urllib.parse | 46 | # +-10% random jitter is added to each Fetches retry sleep duration. |
50 | else: | 47 | RETRY_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 | ||
58 | def _lwrite(path, content): | 50 | def _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): | |||
85 | def sq(r): | 78 | def 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): | |||
234 | class StatusColoring(Coloring): | 232 | class 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): | |||
248 | class DiffColoring(Coloring): | 246 | class 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 | ||
256 | class _Annotation(object): | 254 | class 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 | |||
278 | def _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 | ||
264 | class _CopyFile(object): | 319 | class _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 | ||
294 | class _LinkFile(object): | 366 | class _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 | ||
371 | class 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 = """ | ||
615 | import json, os, sys | ||
616 | path = '''%(path)s''' | ||
617 | kwargs = json.loads('''%(kwargs)s''') | ||
618 | context = json.loads('''%(context)s''') | ||
619 | sys.path.insert(0, os.path.dirname(path)) | ||
620 | data = open(path).read() | ||
621 | exec(compile(data, path, 'exec'), context) | ||
622 | context['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 | |||
770 | class Project(object): | 458 | class 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): | |||
3109 | class _SyncColoring(Coloring): | 3162 | class _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 | |||
17 | import sys | ||
18 | |||
19 | def 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 @@ | |||
1 | These are helper tools for managing official releases. | ||
2 | See 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 | |||
18 | This is intended to be run only by the official Repo release managers. | ||
19 | """ | ||
20 | |||
21 | import argparse | ||
22 | import os | ||
23 | import subprocess | ||
24 | import sys | ||
25 | |||
26 | import util | ||
27 | |||
28 | |||
29 | def 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 | |||
47 | def check(opts): | ||
48 | """Check the signature.""" | ||
49 | util.run(opts, ['gpg', '--verify', f'{opts.launcher}.asc']) | ||
50 | |||
51 | |||
52 | def postmsg(opts): | ||
53 | """Helpful info to show at the end for release manager.""" | ||
54 | print(f""" | ||
55 | Repo launcher bucket: | ||
56 | gs://git-repo-downloads/ | ||
57 | |||
58 | To upload this launcher directly: | ||
59 | gsutil cp -a public-read {opts.launcher} {opts.launcher}.asc gs://git-repo-downloads/ | ||
60 | |||
61 | NB: 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 | |||
67 | def 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 | |||
85 | def 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 | |||
113 | if __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 | |||
18 | This is intended to be run only by the official Repo release managers, but it | ||
19 | could be run by people maintaining their own fork of the project. | ||
20 | |||
21 | NB: Check docs/release-process.md for production freeze information. | ||
22 | """ | ||
23 | |||
24 | import argparse | ||
25 | import os | ||
26 | import re | ||
27 | import subprocess | ||
28 | import sys | ||
29 | |||
30 | import util | ||
31 | |||
32 | |||
33 | # We currently sign with the old DSA key as it's been around the longest. | ||
34 | # We should transition to RSA by Jun 2020, and ECC by Jun 2021. | ||
35 | KEYID = util.KEYID_DSA | ||
36 | |||
37 | # Regular expression to validate tag names. | ||
38 | RE_VALID_TAG = r'^v([0-9]+[.])+[0-9]+$' | ||
39 | |||
40 | |||
41 | def sign(opts): | ||
42 | """Tag the commit & sign it!""" | ||
43 | # We use ! at the end of the key so that gpg uses this specific key. | ||
44 | # Otherwise it uses the key as a lookup into the overall key and uses the | ||
45 | # default signing key. i.e. It will see that KEYID_RSA is a subkey of | ||
46 | # another key, and use the primary key to sign instead of the subkey. | ||
47 | cmd = ['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 | |||
61 | def check(opts): | ||
62 | """Check the signature.""" | ||
63 | util.run(opts, ['git', 'tag', '--verify', opts.tag]) | ||
64 | |||
65 | |||
66 | def 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""" | ||
78 | Here's the short log since the last release. | ||
79 | {shortlog} | ||
80 | |||
81 | To push release to the public: | ||
82 | git push origin {opts.commit}:stable {opts.tag} -n | ||
83 | NB: People will start upgrading to this version immediately. | ||
84 | |||
85 | To roll back a release: | ||
86 | git push origin --force {current_release}:stable -n | ||
87 | """) | ||
88 | |||
89 | |||
90 | def 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 | |||
113 | def 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 | |||
139 | if __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 | |||
18 | This is intended to be run before every official Repo release. | ||
19 | """ | ||
20 | |||
21 | from pathlib import Path | ||
22 | from functools import partial | ||
23 | import argparse | ||
24 | import multiprocessing | ||
25 | import os | ||
26 | import re | ||
27 | import shutil | ||
28 | import subprocess | ||
29 | import sys | ||
30 | import tempfile | ||
31 | |||
32 | TOPDIR = Path(__file__).resolve().parent.parent | ||
33 | MANDIR = TOPDIR.joinpath('man') | ||
34 | |||
35 | # Load repo local modules. | ||
36 | sys.path.insert(0, str(TOPDIR)) | ||
37 | from git_command import RepoSourceVersion | ||
38 | import subcmds | ||
39 | |||
40 | def worker(cmd, **kwargs): | ||
41 | subprocess.run(cmd, **kwargs) | ||
42 | |||
43 | def 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 | |||
101 | if __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 | |||
17 | import os | ||
18 | import re | ||
19 | import subprocess | ||
20 | import sys | ||
21 | |||
22 | |||
23 | assert sys.version_info >= (3, 6), 'This module requires Python 3.6+' | ||
24 | |||
25 | |||
26 | TOPDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | ||
27 | HOMEDIR = os.path.expanduser('~') | ||
28 | |||
29 | |||
30 | # These are the release keys we sign with. | ||
31 | KEYID_DSA = '8BB9AD793E8E6153AF0F9A4416530D5E920F5C65' | ||
32 | KEYID_RSA = 'A34A13BE8E76BFF46A0C022DA2E75A824AAB9624' | ||
33 | KEYID_ECC = 'E1F9040D7A3F6DAFAC897CD3D3B95DA243E48A39' | ||
34 | |||
35 | |||
36 | def 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 | |||
46 | def 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 | |||
58 | def 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')) | ||
@@ -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 | ||
11 | from __future__ import print_function | 25 | from __future__ import print_function |
12 | 26 | ||
27 | import datetime | ||
28 | import os | ||
29 | import platform | ||
30 | import shlex | ||
31 | import subprocess | ||
32 | import 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. | ||
38 | MIN_PYTHON_VERSION_SOFT = (3, 6) | ||
39 | MIN_PYTHON_VERSION_HARD = (3, 5) | ||
40 | |||
41 | |||
42 | # Keep basic logic in sync with repo_trace.py. | ||
43 | class 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 | |||
59 | trace = Trace() | ||
60 | |||
61 | |||
62 | def 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 | |||
75 | def 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 | |||
136 | if __name__ == '__main__': | ||
137 | check_python_version() | ||
138 | |||
139 | |||
13 | # repo default configuration | 140 | # repo default configuration |
14 | # | 141 | # |
15 | import os | ||
16 | REPO_URL = os.environ.get('REPO_URL', None) | 142 | REPO_URL = os.environ.get('REPO_URL', None) |
17 | if not REPO_URL: | 143 | if not REPO_URL: |
18 | REPO_URL = 'https://gerrit.googlesource.com/git-repo' | 144 | REPO_URL = 'https://gerrit.googlesource.com/git-repo' |
19 | REPO_REV = os.environ.get('REPO_REV') | 145 | REPO_REV = os.environ.get('REPO_REV') |
20 | if not REPO_REV: | 146 | if 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. | 149 | BUG_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 |
38 | VERSION = (1, 27) | 152 | VERSION = (2, 17) |
39 | 153 | ||
40 | # increment this if the MAINTAINER_KEYS block is modified | 154 | # increment this if the MAINTAINER_KEYS block is modified |
41 | KEYRING_VERSION = (1, 2) | 155 | KEYRING_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----- |
49 | Version: GnuPG v1.4.2.2 (GNU/Linux) | ||
50 | 163 | ||
51 | mQGiBEj3ugERBACrLJh/ZPyVSKeClMuznFIrsQ+hpNnmJGw1a9GXKYKk8qHPhAZf | 164 | mQGiBEj3ugERBACrLJh/ZPyVSKeClMuznFIrsQ+hpNnmJGw1a9GXKYKk8qHPhAZf |
52 | WKtrBqAVMNRLhL85oSlekRz98u41H5si5zcuv+IXJDF5MJYcB8f22wAy15lUqPWi | 165 | WKtrBqAVMNRLhL85oSlekRz98u41H5si5zcuv+IXJDF5MJYcB8f22wAy15lUqPWi |
@@ -82,63 +195,64 @@ p3v5ILwfC7hVx4jHSnOgZ65L9s8EQdVr1ckN9243yta7rNgwfcqb60ILMFF1BRk/ | |||
82 | 5xGrFy8tfAaeBMIQ17gvFSp/suc9DYO0ICK2BISzq+F+ZiAKsjMYOBNdH/h0zobQ | 195 | 5xGrFy8tfAaeBMIQ17gvFSp/suc9DYO0ICK2BISzq+F+ZiAKsjMYOBNdH/h0zobQ |
83 | HTHs37+/QLMomGEGKZMWi0dShU2J5mNRQu3Hhxl3hHDVbt5CeJBb26aQcQrFz69W | 196 | HTHs37+/QLMomGEGKZMWi0dShU2J5mNRQu3Hhxl3hHDVbt5CeJBb26aQcQrFz69W |
84 | zE3GNvmJosh6leayjtI9P2A6iEkEGBECAAkFAkj3uiACGwwACgkQFlMNXpIPXGWp | 197 | zE3GNvmJosh6leayjtI9P2A6iEkEGBECAAkFAkj3uiACGwwACgkQFlMNXpIPXGWp |
85 | TACbBS+Up3RpfYVfd63c1cDdlru13pQAn3NQy/SN858MkxN+zym86UBgOad2 | 198 | TACbBS+Up3RpfYVfd63c1cDdlru13pQAn3NQy/SN858MkxN+zym86UBgOad2uQIN |
86 | =CMiZ | 199 | BF5FqOoBEAC8aRtWEtXzeuoQhdFrLTqYs2dy6kl9y+j3DMQYAMs8je582qzUigIO |
87 | -----END PGP PUBLIC KEY BLOCK----- | 200 | ZZxq7T/3WQgghsdw9yPvdzlw9tKdet2TJkR1mtBfSjZQrkKwR0pQP4AD7t/90Whu |
88 | 201 | R8Wlu8ysapE2hLxMH5Y2znRQX2LkUYmk0K2ik9AgZEh3AFEg3YLl2pGnSjeSp3ch | |
89 | Conley Owens <cco3@android.com> | 202 | cLX2n/rVZf5LXluZGRG+iov1Ka+8m+UqzohMA1DYNECJW6KPgXsNX++i8/iwZVic |
90 | -----BEGIN PGP PUBLIC KEY BLOCK----- | 203 | PWzhRJSQC+QiAZNsKT6HNNKs97YCUVzhjBLnRSxRBPkr0hS/VMWY2V4pbASljWyd |
91 | Version: GnuPG v1.4.11 (GNU/Linux) | 204 | GYmlDcxheLne0yjes0bJAdvig5rB42FOV0FCM4bDYOVwKfZ7SpzGCYXxtlwe0XNG |
92 | 205 | tLW9WA6tICVqNZ/JNiRTBLrsGSkyrEhDPKnIHlHRI5Zux6IHwMVB0lQKHjSop+t6 | |
93 | mQENBFHRvc8BCADFg45Xx/y6QDC+T7Y/gGc7vx0ww7qfOwIKlAZ9xG3qKunMxo+S | 206 | oyubqWcPCGGYdz2QGQHNz7huC/Zn0wS4hsoiSwPv6HCq3jNyUkOJ7wZ3ouv60p2I |
94 | hPCnzEl3cq+6I1Ww/ndop/HB3N3toPXRCoN8Vs4/Hc7by+SnaLFnacrm+tV5/OgT | 207 | kPurgviVaRaPSKTYdKfkcJOtFeqOh1na5IHkXsD9rNctB7tSgfsm0G6qJIVe3ZmJ |
95 | V37Lzt8lhay1Kl+YfpFwHYYpIEBLFV9knyfRXS/428W2qhdzYfvB15/AasRmwmor | 208 | 7QAyHBfuLrAWCq5xS8EHDlvxPdAD8EEsa9T32YxcHKIkxr1eSwrUrKb8cPhWq1pp |
96 | py4NIzSs8UD/SPr1ihqNCdZM76+MQyN5HMYXW/ALZXUFG0pwluHFA7hrfPG74i8C | 209 | Jiylw6G1fZ02VKixqmPC4oFMyg1PO8L2tcQTrnVmZvfFGiaekHKdhQARAQABiQKW |
97 | zMiP7qvMWIl/r/jtzHioH1dRKgbod+LZsrDJ8mBaqsZaDmNJMhss9g76XvfMyLra | 210 | BBgRAgAgFiEEi7mteT6OYVOvD5pEFlMNXpIPXGUFAl5FqOoCGwICQAkQFlMNXpIP |
98 | 9DI9/iFuBpGzeqBv0hwOGQspLRrEoyTeR6n1ABEBAAG0H0NvbmxleSBPd2VucyA8 | 211 | XGXBdCAEGQEKAB0WIQSjShO+jna/9GoMAi2i51qCSquWJAUCXkWo6gAKCRCi51qC |
99 | Y2NvM0BhbmRyb2lkLmNvbT6JATgEEwECACIFAlHRvc8CGwMGCwkIBwMCBhUIAgkK | 212 | SquWJLzgD/0YEZYS7yKxhP+kk94TcTYMBMSZpU5KFClB77yu4SI1LeXq4ocBT4sp |
100 | CwQWAgMBAh4BAheAAAoJEGe35EhpKzgsP6AIAJKJmNtn4l7hkYHKHFSo3egb6RjQ | 213 | EPaOsQiIx//j59J67b7CBe4UeRA6D2n0pw+bCKuc731DFi5X9C1zq3a7E67SQ2yd |
101 | zEIP3MFTcu8HFX1kF1ZFbrp7xqurLaE53kEkKuAAvjJDAgI8mcZHP1JyplubqjQA | 214 | FbYE2fnpVnMqb62g4sTh7JmdxEtXCWBUWL0OEoWouBW1PkFDHx2kYLC7YpZt3+4t |
102 | xvv84gK+OGP3Xk+QK1ZjUQSbjOpjEiSZpRhWcHci3dgOUH4blJfByHw25hlgHowd | 215 | VtNhSfV8NS6PF8ep3JXHVd2wsC3DQtggeId5GM44o8N0SkwQHNjK8ZD+VZ74ZnhZ |
103 | a/2PrNKZVcJ92YienaxxGjcXEUcd0uYEG2+rwllQigFcnMFDhr9B71MfalRHjFKE | 216 | HeyHskomiOC61LrZWQvxD6VqtfnBQ5GvONO8QuhkiFwMMOnpPVj2k7ngSkd5o27K |
104 | fmdoypqLrri61YBc59P88Rw2/WUpTQjgNubSqa3A2+CKdaRyaRw+2fdF4TdR0h8W | 217 | 6c53ZESOlR4bAfl0i3RZYC9B5KerGkBE3dTgTzmGjOaahl2eLz4LDPdTwMtS+sAU |
105 | zbg+lbaPtJHsV+3mJC7fq26MiJDRJa5ZztpMn8su20gbLgi2ShBOaHAYDDi5AQ0E | 218 | 1hPPvZTQeYDdV62bOWUyteMoJu354GgZPQ9eItWYixpNCyOGNcJXl6xk3/OuoP6f |
106 | UdG9zwEIAMoOBq+QLNozAhxOOl5GL3StTStGRgPRXINfmViTsihrqGCWBBUfXlUE | 219 | MciFV8aMxs/7mUR8q1Ei3X9MKu+bbODYj2rC1tMkLj1OaAJkfvRuYrKsQpoUsn4q |
107 | OytC0mYcrDUQev/8ToVoyqw+iGSwDkcSXkrEUCKFtHV/GECWtk1keyHgR10YKI1R | 220 | VT9+aciNpU/I7M30watlWo7RfUFI3zaGdMDcMFju1cWt2Un8E3gtscGufzbz1Z5Z |
108 | mquSXoubWGqPeG1PAI74XWaRx8UrL8uCXUtmD8Q5J7mDjKR5NpxaXrwlA0bKsf2E | 221 | Gak+tCOWUyuYNWX3noit7Dk6+3JGHGaQettldNu2PLM9SbIXd2EaqK/eEv9BS3dd |
109 | Gp9tu1kKauuToZhWHMRMqYSOGikQJwWSFYKT1KdNcOXLQF6+bfoJ6sjVYdwfmNQL | 222 | ItkZwzyZXSaQ9UqAceY1AHskJJ5KVXIRLuhP5jBWWo3fnRMyMYt2nwNBAJ9B9TA8 |
110 | Ixn8QVhoTDedcqClSWB17VDEFDFa7MmqXZz2qtM3X1R/MUMHqPtegQzBGNhRdnI2 | 223 | VlBniwIl5EzCvOFOTGrtewCdHOvr3N3ieypGz1BzyCN9tJMO3G24MwReRal9Fgkr |
111 | V45+1Nnx/uuCxDbeI4RbHzujnxDiq70AEQEAAYkBHwQYAQIACQUCUdG9zwIbDAAK | 224 | BgEEAdpHDwEBB0BhPE/je6OuKgWzJ1mnrUmHhn4IMOHp+58+T5kHU3Oy6YjXBBgR |
112 | CRBnt+RIaSs4LNVeB/0Y2pZ8I7gAAcEM0Xw8drr4omg2fUoK1J33ozlA/RxeA/lJ | 225 | AgAgFiEEi7mteT6OYVOvD5pEFlMNXpIPXGUFAl5FqX0CGwIAgQkQFlMNXpIPXGV2 |
113 | I3KnyCDTpXuIeBKPGkdL8uMATC9Z8DnBBajRlftNDVZS3Hz4G09G9QpMojvJkFJV | 226 | IAQZFggAHRYhBOH5BA16P22vrIl809O5XaJD5Io5BQJeRal9AAoJENO5XaJD5Io5 |
114 | By+01Flw/X+eeN8NpqSuLV4W+AjEO8at/VvgKr1AFvBRdZ7GkpI1o6DgPe7ZqX+1 | 227 | MEkA/3uLmiwANOcgE0zB9zga0T/KkYhYOWFx7zRyDhrTf9spAPwIfSBOAGtwxjLO |
115 | dzQZt3e13W0rVBb/bUgx9iSLoeWP3aq/k+/GRGOR+S6F6BBSl0SQ2EF2+dIywb1x | 228 | DCce5OaQJl/YuGHvXq2yx5h7T8pdAZ+PAJ4qfIk2LLSidsplTDXOKhOQAuOqUQCf |
116 | JuinEP+AwLAUZ1Bsx9ISC0Agpk2VeHXPL3FGhroEmoMvBzO0kTFGyoeT7PR/BfKv | 229 | cZ7aFsJF4PtcDrfdejyAxbtsSHI= |
117 | +H/g3HsL2LOB9uoIm8/5p2TTU5ttYCXMHhQZ81AY | 230 | =82Tj |
118 | =AUp4 | ||
119 | -----END PGP PUBLIC KEY BLOCK----- | 231 | -----END PGP PUBLIC KEY BLOCK----- |
120 | """ | 232 | """ |
121 | 233 | ||
122 | GIT = 'git' # our git command | 234 | GIT = '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. | ||
123 | MIN_GIT_VERSION = (1, 7, 2) # minimum supported git version | 240 | MIN_GIT_VERSION = (1, 7, 2) # minimum supported git version |
124 | repodir = '.repo' # name of repo's private directory | 241 | repodir = '.repo' # name of repo's private directory |
125 | S_repo = 'repo' # special repo repository | 242 | S_repo = 'repo' # special repo repository |
126 | S_manifests = 'manifests' # special manifest repository | 243 | S_manifests = 'manifests' # special manifest repository |
127 | REPO_MAIN = S_repo + '/main.py' # main script | 244 | REPO_MAIN = S_repo + '/main.py' # main script |
128 | MIN_PYTHON_VERSION = (2, 7) # minimum supported python version | ||
129 | GITC_CONFIG_FILE = '/gitc/.config' | 245 | GITC_CONFIG_FILE = '/gitc/.config' |
130 | GITC_FS_ROOT_DIR = '/gitc/manifest-rw/' | 246 | GITC_FS_ROOT_DIR = '/gitc/manifest-rw/' |
131 | 247 | ||
132 | 248 | ||
133 | import collections | 249 | import collections |
134 | import errno | 250 | import errno |
251 | import json | ||
135 | import optparse | 252 | import optparse |
136 | import platform | ||
137 | import re | 253 | import re |
138 | import shutil | 254 | import shutil |
139 | import stat | 255 | import stat |
140 | import subprocess | ||
141 | import sys | ||
142 | 256 | ||
143 | if sys.version_info[0] == 3: | 257 | if 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 | ||
155 | ver = sys.version_info | ||
156 | if (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 | |||
165 | home_dot_repo = os.path.expanduser('~/.repoconfig') | 268 | home_dot_repo = os.path.expanduser('~/.repoconfig') |
166 | gpg_dir = os.path.join(home_dot_repo, 'gnupg') | 269 | gpg_dir = os.path.join(home_dot_repo, 'gnupg') |
167 | 270 | ||
168 | extra_args = [] | 271 | |
169 | init_optparse = optparse.OptionParser(usage="repo init -u url [options]") | 272 | def GetParser(gitc_init=False): |
170 | 273 | """Setup the CLI parser.""" | |
171 | # Logging | 274 | if gitc_init: |
172 | group = init_optparse.add_option_group('Logging options') | 275 | usage = 'repo gitc-init -c client [options] [-u] url' |
173 | group.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) |
178 | group = init_optparse.add_option_group('Manifest options') | 281 | return parser |
179 | group.add_option('-u', '--manifest-url', | 282 | |
180 | dest='manifest_url', | 283 | |
181 | help='manifest repository location', metavar='URL') | 284 | def InitParser(parser, gitc_init=False): |
182 | group.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 | |
185 | group.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', |
188 | group.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', |
191 | group.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. |
195 | group.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') |
198 | group.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', |
201 | group.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 ' |
204 | group.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 ' |
208 | group.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', |
211 | group.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 ' |
215 | group.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 |
218 | group.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. |
223 | group.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') |
228 | group.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') |
231 | group.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") |
237 | group = init_optparse.add_option_group('repo Version options') | 340 | |
238 | group.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', |
241 | group.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', |
244 | group.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 | |
249 | group = init_optparse.add_option_group('Other options') | 352 | # These are fundamentally different ways of structuring the checkout. |
250 | group.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') | |
255 | def _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+. | ||
416 | RunResult = collections.namedtuple( | ||
417 | 'RunResult', ('returncode', 'stdout', 'stderr')) | ||
418 | |||
419 | |||
420 | class RunError(Exception): | ||
421 | """Error when running a command failed.""" | ||
422 | |||
423 | |||
424 | def 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(): | |||
283 | def gitc_parse_clientdir(gitc_fs_path): | 496 | def 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 | ||
527 | def 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 | |||
541 | def 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 | |||
312 | def _Init(args, gitc_init=False): | 555 | def _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 | ||
627 | def 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. |
402 | GitVersion = collections.namedtuple( | 647 | GitVersion = collections.namedtuple( |
403 | 'GitVersion', ('major', 'minor', 'micro', 'full')) | 648 | 'GitVersion', ('major', 'minor', 'micro', 'full')) |
404 | 649 | ||
650 | |||
405 | def ParseGitVersion(ver_str=None): | 651 | def 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 | ||
425 | def _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 | |||
444 | def _CheckGitVersion(): | 671 | def _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 | ||
684 | def 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 | |||
706 | def _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 | |||
460 | def NeedSetupGnuPG(): | 717 | def 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 | ||
526 | def _SetConfig(local, name, value): | 778 | def _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 | |
784 | def _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 | ||
534 | def _InitHttp(): | 802 | def _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 | ||
559 | def _Fetch(url, local, src, quiet): | 827 | def _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 | ||
581 | def _DownloadBundle(url, local, quiet): | 842 | def _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 | ||
633 | def _ImportBundle(local): | 888 | def _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 | ||
641 | def _Clone(url, local, quiet, clone_bundle): | 896 | def _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 | |||
921 | def 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 | ||
676 | def _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 | |||
986 | def 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 | ||
731 | def _Checkout(cwd, branch, rev, quiet): | 1006 | def _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 | ||
753 | def _FindRepo(): | 1023 | def _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 | ||
771 | class _Options(object): | 1039 | class _Options(object): |
772 | help = False | 1040 | help = False |
1041 | version = False | ||
1042 | |||
1043 | |||
1044 | def _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 | ||
775 | def _ParseArguments(args): | 1063 | def _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 | ||
1083 | class 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 | |||
792 | def _Usage(): | 1167 | def _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 | ||
808 | For access to the full online help, install repo ("repo init"). | 1183 | For 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 | ||
813 | def _Help(args): | 1189 | def _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 | ||
1204 | def _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 | |||
831 | def _NotInstalled(): | 1223 | def _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 | ||
880 | def main(orig_args): | 1269 | def 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 | ||
942 | if __name__ == '__main__': | 1342 | if __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 @@ | |||
19 | Activated via `repo --trace ...` or `REPO_TRACE=1 repo ...`. | 17 | Activated via `repo --trace ...` or `REPO_TRACE=1 repo ...`. |
20 | """ | 18 | """ |
21 | 19 | ||
22 | from __future__ import print_function | ||
23 | import sys | 20 | import sys |
24 | import os | 21 | import 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 | |||
31 | def IsTrace(): | 29 | def IsTrace(): |
32 | return _TRACE | 30 | return _TRACE |
33 | 31 | ||
32 | |||
34 | def SetTrace(): | 33 | def SetTrace(): |
35 | global _TRACE | 34 | global _TRACE |
36 | _TRACE = True | 35 | _TRACE = True |
37 | 36 | ||
37 | |||
38 | def Trace(fmt, *args): | 38 | def 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 | } | ||
@@ -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 | ||
19 | from __future__ import print_function | ||
20 | |||
21 | import errno | 18 | import errno |
22 | import os | 19 | import os |
20 | import shutil | ||
23 | import subprocess | 21 | import subprocess |
24 | import sys | 22 | import sys |
25 | 23 | ||
26 | 24 | ||
27 | def run_pytest(cmd, argv): | 25 | def 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 | ||
41 | def main(argv): | 46 | def 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 | ||
52 | if __name__ == '__main__': | 60 | if __name__ == '__main__': |
@@ -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 | ||
19 | from __future__ import print_function | ||
20 | |||
21 | import os | 18 | import os |
22 | import setuptools | 19 | import 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/ |
36 | setuptools.setup( | 33 | setuptools.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 | ) |
@@ -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 | |||
17 | import functools | ||
18 | import multiprocessing | ||
19 | import os | ||
20 | import re | ||
21 | import signal | ||
22 | import subprocess | ||
23 | import sys | ||
24 | import tempfile | ||
25 | import time | ||
26 | |||
27 | import platform_utils | ||
28 | from repo_trace import Trace | ||
29 | |||
30 | |||
31 | PROXY_PATH = os.path.join(os.path.dirname(__file__), 'git_ssh') | ||
32 | |||
33 | |||
34 | def _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 | |||
39 | def _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) | ||
51 | def 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 | |||
60 | URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):') | ||
61 | URI_ALL = re.compile(r'^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/') | ||
62 | |||
63 | |||
64 | class 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 | ||
17 | import os | 15 | import os |
18 | 16 | ||
17 | # A mapping of the subcommand name to the class that implements it. | ||
19 | all_commands = {} | 18 | all_commands = {} |
20 | 19 | ||
21 | my_dir = os.path.dirname(__file__) | 20 | my_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 | ||
49 | if 'help' in all_commands: | 48 | # Add 'branch' as an alias for 'branches'. |
50 | all_commands['help'].commands = all_commands | 49 | all_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 | ||
17 | from __future__ import print_function | ||
18 | import sys | ||
19 | from command import Command | ||
20 | from collections import defaultdict | 15 | from collections import defaultdict |
16 | import functools | ||
17 | import itertools | ||
18 | import sys | ||
19 | |||
20 | from command import Command, DEFAULT_LOCAL_JOBS | ||
21 | from git_command import git | 21 | from git_command import git |
22 | from progress import Progress | 22 | from progress import Progress |
23 | 23 | ||
24 | |||
24 | class Abandon(Command): | 25 | class 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 | ||
33 | It is equivalent to "git branch -D <branchname>". | 34 | It 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 | ||
17 | from __future__ import print_function | 15 | import itertools |
18 | import sys | 16 | import sys |
17 | |||
19 | from color import Coloring | 18 | from color import Coloring |
20 | from command import Command | 19 | from command import Command, DEFAULT_LOCAL_JOBS |
20 | |||
21 | 21 | ||
22 | class BranchColoring(Coloring): | 22 | class 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 | |||
29 | class BranchInfo(object): | 30 | class 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 | ||
63 | class Branches(Command): | 64 | class 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 | |||
94 | is shown, then the branch appears in all projects. | 95 | is 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 | |||
190 | def 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 | ||
17 | from __future__ import print_function | 15 | import functools |
18 | import sys | 16 | import sys |
19 | from command import Command | 17 | |
18 | from command import Command, DEFAULT_LOCAL_JOBS | ||
20 | from progress import Progress | 19 | from progress import Progress |
21 | 20 | ||
21 | |||
22 | class Checkout(Command): | 22 | class 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 | ||
17 | from __future__ import print_function | ||
18 | import re | 15 | import re |
19 | import sys | 16 | import sys |
20 | from command import Command | 17 | from command import Command |
@@ -22,8 +19,9 @@ from git_command import GitCommand | |||
22 | 19 | ||
23 | CHANGE_ID_RE = re.compile(r'^\s*Change-Id: I([0-9a-f]{40})\s*$') | 20 | CHANGE_ID_RE = re.compile(r'^\s*Change-Id: I([0-9a-f]{40})\s*$') |
24 | 21 | ||
22 | |||
25 | class CherryPick(Command): | 23 | class 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 | |||
34 | change id will be added. | 32 | change 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 | ||
17 | from command import PagedCommand | 15 | import functools |
16 | import io | ||
17 | |||
18 | from command import DEFAULT_LOCAL_JOBS, PagedCommand | ||
19 | |||
18 | 20 | ||
19 | class Diff(PagedCommand): | 21 | class 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 | |||
26 | relative to the repository root, so the output can be applied | 28 | relative to the repository root, so the output can be applied |
27 | to the Unix 'patch' command. | 29 | to 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 | ||
17 | from color import Coloring | 15 | from color import Coloring |
18 | from command import PagedCommand | 16 | from command import PagedCommand |
19 | from manifest_xml import XmlManifest | 17 | from manifest_xml import RepoClient |
18 | |||
20 | 19 | ||
21 | class _Coloring(Coloring): | 20 | class _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 | |||
25 | class Diffmanifests(PagedCommand): | 25 | class 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 | ||
17 | from __future__ import print_function | ||
18 | import re | 15 | import re |
19 | import sys | 16 | import sys |
20 | 17 | ||
21 | from command import Command | 18 | from command import Command |
22 | from error import GitError | 19 | from error import GitError, NoSuchProjectError |
23 | 20 | ||
24 | CHANGE_RE = re.compile(r'^([1-9][0-9]*)(?:[/\.-]([1-9][0-9]*))?$') | 21 | CHANGE_RE = re.compile(r'^([1-9][0-9]*)(?:[/\.-]([1-9][0-9]*))?$') |
25 | 22 | ||
23 | |||
26 | class Download(Command): | 24 | class 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 | ||
17 | from __future__ import print_function | ||
18 | import errno | 15 | import errno |
16 | import functools | ||
17 | import io | ||
19 | import multiprocessing | 18 | import multiprocessing |
20 | import re | 19 | import re |
21 | import os | 20 | import os |
@@ -24,14 +23,14 @@ import sys | |||
24 | import subprocess | 23 | import subprocess |
25 | 24 | ||
26 | from color import Coloring | 25 | from color import Coloring |
27 | from command import Command, MirrorSafeCommand | 26 | from command import DEFAULT_LOCAL_JOBS, Command, MirrorSafeCommand, WORKER_BATCH_SIZE |
28 | import platform_utils | 27 | from 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 | ||
44 | class Forall(Command, MirrorSafeCommand): | 43 | class 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 = """ |
52 | Executes the same shell command in each project. | 51 | Executes the same shell command in each project. |
@@ -54,6 +53,11 @@ Executes the same shell command in each project. | |||
54 | The -r option allows running the command only on projects matching | 53 | The -r option allows running the command only on projects matching |
55 | regex or wildcard expression. | 54 | regex or wildcard expression. |
56 | 55 | ||
56 | By default, projects are processed non-interactively in parallel. If you want | ||
57 | to run interactive commands, make sure to pass --interactive to force --jobs 1. | ||
58 | While the processing order of projects is not guaranteed, the order of project | ||
59 | output is stable. | ||
60 | |||
57 | # Output Formatting | 61 | # Output Formatting |
58 | 62 | ||
59 | The -p option causes '%prog' to bind pipes to the command's stdin, | 63 | The -p option causes '%prog' to bind pipes to the command's stdin, |
@@ -116,70 +120,48 @@ terminal and are not redirected. | |||
116 | If -e is used, when a command exits unsuccessfully, '%prog' will abort | 120 | If -e is used, when a command exits unsuccessfully, '%prog' will abort |
117 | without iterating through the remaining projects. | 121 | without 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 | ||
280 | class WorkerKeyboardInterrupt(Exception): | 259 | class WorkerKeyboardInterrupt(Exception): |
281 | """ Keyboard interrupt exception for worker processes. """ | 260 | """ Keyboard interrupt exception for worker processes. """ |
282 | pass | ||
283 | 261 | ||
284 | 262 | ||
285 | def InitWorker(): | 263 | def InitWorker(): |
286 | signal.signal(signal.SIGINT, signal.SIG_IGN) | 264 | signal.signal(signal.SIGINT, signal.SIG_IGN) |
287 | 265 | ||
288 | def DoWorkWrapper(args): | 266 | |
267 | def 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 | ||
304 | def DoWork(project, mirror, opt, cmd, shell, cnt, config): | 283 | def 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 | ||
17 | from __future__ import print_function | ||
18 | import sys | 15 | import sys |
19 | 16 | ||
20 | from command import Command, GitcClientCommand | 17 | from command import Command, GitcClientCommand |
21 | import platform_utils | 18 | import platform_utils |
22 | 19 | ||
23 | from pyversion import is_python3 | ||
24 | if not is_python3(): | ||
25 | input = raw_input | ||
26 | 20 | ||
27 | class GitcDelete(Command, GitcClientCommand): | 21 | class 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 | ||
17 | from __future__ import print_function | ||
18 | import os | 15 | import os |
19 | import sys | 16 | import sys |
20 | 17 | ||
@@ -26,7 +23,7 @@ import wrapper | |||
26 | 23 | ||
27 | 24 | ||
28 | class GitcInit(init.Init, GitcAvailableCommand): | 25 | class 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 | ||
17 | from __future__ import print_function | 15 | import functools |
18 | |||
19 | import sys | 16 | import sys |
20 | 17 | ||
21 | from color import Coloring | 18 | from color import Coloring |
22 | from command import PagedCommand | 19 | from command import DEFAULT_LOCAL_JOBS, PagedCommand |
23 | from error import GitError | 20 | from error import GitError |
24 | from git_command import git_require, GitCommand | 21 | from git_command import GitCommand |
22 | |||
25 | 23 | ||
26 | class GrepColoring(Coloring): | 24 | class 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 | |||
32 | class Grep(PagedCommand): | 31 | class 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 | ||
17 | from __future__ import print_function | ||
18 | import re | 15 | import re |
19 | import sys | 16 | import sys |
20 | from formatter import AbstractFormatter, DumbWriter | 17 | import textwrap |
21 | 18 | ||
19 | from subcmds import all_commands | ||
22 | from color import Coloring | 20 | from color import Coloring |
23 | from command import PagedCommand, MirrorSafeCommand, GitcAvailableCommand, GitcClientCommand | 21 | from command import PagedCommand, MirrorSafeCommand, GitcAvailableCommand, GitcClientCommand |
24 | import gitc_utils | 22 | import gitc_utils |
23 | from wrapper import Wrapper | ||
24 | |||
25 | 25 | ||
26 | class Help(PagedCommand, MirrorSafeCommand): | 26 | class 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 | ||
15 | import optparse | ||
16 | |||
17 | from command import PagedCommand | 17 | from command import PagedCommand |
18 | from color import Coloring | 18 | from color import Coloring |
19 | from git_refs import R_M | 19 | from git_refs import R_M, R_HEADS |
20 | |||
20 | 21 | ||
21 | class _Coloring(Coloring): | 22 | class _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 | |||
25 | class Info(PagedCommand): | 27 | class 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 | ||
17 | from __future__ import print_function | ||
18 | import os | 15 | import os |
19 | import platform | 16 | import platform |
20 | import re | 17 | import re |
18 | import subprocess | ||
21 | import sys | 19 | import sys |
22 | 20 | import urllib.parse | |
23 | from pyversion import is_python3 | ||
24 | if is_python3(): | ||
25 | import urllib.parse | ||
26 | else: | ||
27 | import imp | ||
28 | import urlparse | ||
29 | urllib = imp.new_module('urllib') | ||
30 | urllib.parse = urlparse | ||
31 | 21 | ||
32 | from color import Coloring | 22 | from color import Coloring |
33 | from command import InteractiveCommand, MirrorSafeCommand | 23 | from command import InteractiveCommand, MirrorSafeCommand |
34 | from error import ManifestParseError | 24 | from error import ManifestParseError |
35 | from project import SyncBuffer | 25 | from project import SyncBuffer |
36 | from git_config import GitConfig | 26 | from git_config import GitConfig |
37 | from git_command import git_require, MIN_GIT_VERSION | 27 | from git_command import git_require, MIN_GIT_VERSION_SOFT, MIN_GIT_VERSION_HARD |
28 | import fetch | ||
29 | import git_superproject | ||
38 | import platform_utils | 30 | import platform_utils |
31 | from wrapper import Wrapper | ||
32 | |||
39 | 33 | ||
40 | class Init(InteractiveCommand, MirrorSafeCommand): | 34 | class 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 = """ |
47 | The '%prog' command is run once to install and initialize repo. | 41 | The '%prog' command is run once to install and initialize repo. |
@@ -49,13 +43,24 @@ The latest repo source code and manifest collection is downloaded | |||
49 | from the server and is installed in the .repo/ directory in the | 43 | from the server and is installed in the .repo/ directory in the |
50 | current working directory. | 44 | current working directory. |
51 | 45 | ||
46 | When creating a new checkout, the manifest URL is the only required setting. | ||
47 | It may be specified using the --manifest-url option, or as the first optional | ||
48 | argument. | ||
49 | |||
52 | The optional -b argument can be used to select the manifest branch | 50 | The optional -b argument can be used to select the manifest branch |
53 | to checkout and use. If no branch is specified, master is assumed. | 51 | to checkout and use. If no branch is specified, the remote's default |
52 | branch is used. This is equivalent to using -b HEAD. | ||
54 | 53 | ||
55 | The optional -m argument can be used to specify an alternate manifest | 54 | The optional -m argument can be used to specify an alternate manifest |
56 | to be used. If no manifest is specified, the manifest default.xml | 55 | to be used. If no manifest is specified, the manifest default.xml |
57 | will be used. | 56 | will be used. |
58 | 57 | ||
58 | If the --standalone-manifest argument is set, the manifest will be downloaded | ||
59 | directly from the specified --manifest-url as a static file (rather than | ||
60 | setting up a manifest git checkout). With --standalone-manifest, the manifest | ||
61 | will be fully static and will not be re-downloaded during subsesquent | ||
62 | `repo init` and `repo sync` calls. | ||
63 | |||
59 | The --reference option can be used to point to a directory that | 64 | The --reference option can be used to point to a directory that |
60 | has the content of a --mirror sync. This will make the working | 65 | has the content of a --mirror sync. This will make the working |
61 | directory use as much data as possible from the local reference | 66 | directory 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 | |||
81 | to update the working directory files. | 86 | to 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 | ||
17 | from __future__ import print_function | 15 | import os |
18 | import sys | ||
19 | 16 | ||
20 | from command import Command, MirrorSafeCommand | 17 | from command import Command, MirrorSafeCommand |
21 | 18 | ||
19 | |||
22 | class List(Command, MirrorSafeCommand): | 20 | class 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 = """ |
30 | List all projects; pass '.' to list the project for the cwd. | 28 | List all projects; pass '.' to list the project for the cwd. |
31 | 29 | ||
30 | By default, only projects that currently exist in the checkout are shown. If | ||
31 | you 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 | ||
33 | groups, then also pass --groups all. | ||
34 | |||
32 | This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'. | 35 | This 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 | ||
17 | from __future__ import print_function | 15 | import json |
18 | import os | 16 | import os |
19 | import sys | 17 | import sys |
20 | 18 | ||
21 | from command import PagedCommand | 19 | from command import PagedCommand |
22 | 20 | ||
21 | |||
23 | class Manifest(PagedCommand): | 22 | class 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 | ||
31 | With the -o option, exports the current manifest for inspection. | 30 | With the -o option, exports the current manifest for inspection. |
32 | The manifest and (if present) local_manifest.xml are combined | 31 | The manifest and (if present) local_manifests/ are combined |
33 | together to produce a single manifest file. This file can be stored | 32 | together to produce a single manifest file. This file can be stored |
34 | in a Git repository for use during future 'repo init' invocations. | 33 | in a Git repository for use during future 'repo init' invocations. |
35 | 34 | ||
35 | The -r option can be used to generate a manifest file with project | ||
36 | revisions set to the current commit hash. These are known as | ||
37 | "revision locked manifests", as they don't follow a particular branch. | ||
38 | In this case, the 'upstream' attribute is set to the ref we were on | ||
39 | when the manifest was generated. The 'dest-branch' attribute is set | ||
40 | to 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 | ||
17 | from __future__ import print_function | 15 | import optparse |
16 | |||
18 | from color import Coloring | 17 | from color import Coloring |
19 | from command import PagedCommand | 18 | from command import PagedCommand |
20 | 19 | ||
21 | 20 | ||
22 | class Overview(PagedCommand): | 21 | class 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): | |||
29 | The '%prog' command is used to display an overview of the projects branches, | 28 | The '%prog' command is used to display an overview of the projects branches, |
30 | and list any local commits that have not yet been merged into the project. | 29 | and list any local commits that have not yet been merged into the project. |
31 | 30 | ||
32 | The -b/--current-branch option can be used to restrict the output to only | 31 | The -c/--current-branch option can be used to restrict the output to only |
33 | branches currently checked out in each project. By default, all branches | 32 | branches currently checked out in each project. By default, all branches |
34 | are displayed. | 33 | are 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 | ||
17 | from __future__ import print_function | 15 | import itertools |
16 | |||
18 | from color import Coloring | 17 | from color import Coloring |
19 | from command import PagedCommand | 18 | from command import DEFAULT_LOCAL_JOBS, PagedCommand |
19 | |||
20 | 20 | ||
21 | class Prune(PagedCommand): | 21 | class 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 | ||
17 | from __future__ import print_function | ||
18 | import sys | 15 | import sys |
19 | 16 | ||
20 | from color import Coloring | 17 | from color import Coloring |
@@ -30,7 +27,7 @@ class RebaseColoring(Coloring): | |||
30 | 27 | ||
31 | 28 | ||
32 | class Rebase(Command): | 29 | class 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 | ||
17 | from __future__ import print_function | ||
18 | from optparse import SUPPRESS_HELP | 15 | from optparse import SUPPRESS_HELP |
19 | import sys | 16 | import sys |
20 | 17 | ||
@@ -22,8 +19,9 @@ from command import Command, MirrorSafeCommand | |||
22 | from subcmds.sync import _PostRepoUpgrade | 19 | from subcmds.sync import _PostRepoUpgrade |
23 | from subcmds.sync import _PostRepoFetch | 20 | from subcmds.sync import _PostRepoFetch |
24 | 21 | ||
22 | |||
25 | class Selfupdate(Command, MirrorSafeCommand): | 23 | class 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 | ||
17 | from subcmds.sync import Sync | 15 | from subcmds.sync import Sync |
18 | 16 | ||
17 | |||
19 | class Smartsync(Sync): | 18 | class 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 | ||
17 | from __future__ import print_function | ||
18 | import sys | 15 | import sys |
19 | 16 | ||
20 | from color import Coloring | 17 | from color import Coloring |
21 | from command import InteractiveCommand | 18 | from command import InteractiveCommand |
22 | from git_command import GitCommand | 19 | from git_command import GitCommand |
23 | 20 | ||
21 | |||
24 | class _ProjectList(Coloring): | 22 | class _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 | |||
31 | class Stage(InteractiveCommand): | 30 | class 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 | |||
108 | def _AddI(project): | 109 | def _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 | ||
17 | from __future__ import print_function | 15 | import functools |
18 | import os | 16 | import os |
19 | import sys | 17 | import sys |
20 | 18 | ||
21 | from command import Command | 19 | from command import Command, DEFAULT_LOCAL_JOBS |
22 | from git_config import IsImmutable | 20 | from git_config import IsImmutable |
23 | from git_command import git | 21 | from git_command import git |
24 | import gitc_utils | 22 | import gitc_utils |
25 | from progress import Progress | 23 | from progress import Progress |
26 | from project import SyncBuffer | 24 | from project import SyncBuffer |
27 | 25 | ||
26 | |||
28 | class Start(Command): | 27 | class 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 |
36 | revision specified in the manifest. | 35 | revision 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 | ||
17 | from __future__ import print_function | 15 | import functools |
18 | |||
19 | from command import PagedCommand | ||
20 | |||
21 | try: | ||
22 | import threading as _threading | ||
23 | except ImportError: | ||
24 | import dummy_threading as _threading | ||
25 | |||
26 | import glob | 16 | import glob |
27 | 17 | import io | |
28 | import itertools | ||
29 | import os | 18 | import os |
30 | 19 | ||
20 | from command import DEFAULT_LOCAL_JOBS, PagedCommand | ||
21 | |||
31 | from color import Coloring | 22 | from color import Coloring |
32 | import platform_utils | 23 | import platform_utils |
33 | 24 | ||
25 | |||
34 | class Status(PagedCommand): | 26 | class 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 | ||
17 | from __future__ import print_function | 15 | import errno |
16 | import functools | ||
17 | import http.cookiejar as cookielib | ||
18 | import io | ||
18 | import json | 19 | import json |
20 | import multiprocessing | ||
19 | import netrc | 21 | import netrc |
20 | from optparse import SUPPRESS_HELP | 22 | from optparse import SUPPRESS_HELP |
21 | import os | 23 | import os |
22 | import re | ||
23 | import socket | 24 | import socket |
24 | import subprocess | ||
25 | import sys | 25 | import sys |
26 | import tempfile | 26 | import tempfile |
27 | import time | 27 | import time |
28 | 28 | import urllib.error | |
29 | from pyversion import is_python3 | 29 | import urllib.parse |
30 | if is_python3(): | 30 | import urllib.request |
31 | import http.cookiejar as cookielib | 31 | import xmlrpc.client |
32 | import urllib.error | ||
33 | import urllib.parse | ||
34 | import urllib.request | ||
35 | import xmlrpc.client | ||
36 | else: | ||
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 | ||
49 | try: | 33 | try: |
50 | import threading as _threading | 34 | import threading as _threading |
@@ -53,44 +37,36 @@ except ImportError: | |||
53 | 37 | ||
54 | try: | 38 | try: |
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) |
58 | except ImportError: | 43 | except ImportError: |
59 | def _rlimit_nofile(): | 44 | def _rlimit_nofile(): |
60 | return (256, 256) | 45 | return (256, 256) |
61 | 46 | ||
62 | try: | ||
63 | import multiprocessing | ||
64 | except ImportError: | ||
65 | multiprocessing = None | ||
66 | |||
67 | import event_log | 47 | import event_log |
68 | from git_command import GIT, git_require | 48 | from git_command import git_require |
69 | from git_config import GetUrlCookieFile | 49 | from git_config import GetUrlCookieFile |
70 | from git_refs import R_HEADS, HEAD | 50 | from git_refs import R_HEADS, HEAD |
51 | import git_superproject | ||
71 | import gitc_utils | 52 | import gitc_utils |
72 | from project import Project | 53 | from project import Project |
73 | from project import RemoteSpec | 54 | from project import RemoteSpec |
74 | from command import Command, MirrorSafeCommand | 55 | from command import Command, MirrorSafeCommand, WORKER_BATCH_SIZE |
75 | from error import RepoChangedException, GitError, ManifestParseError | 56 | from error import RepoChangedException, GitError, ManifestParseError |
76 | import platform_utils | 57 | import platform_utils |
77 | from project import SyncBuffer | 58 | from project import SyncBuffer |
78 | from progress import Progress | 59 | from progress import Progress |
60 | import ssh | ||
79 | from wrapper import Wrapper | 61 | from wrapper import Wrapper |
80 | from manifest_xml import GitcManifest | 62 | from manifest_xml import GitcManifest |
81 | 63 | ||
82 | _ONE_DAY_S = 24 * 60 * 60 | 64 | _ONE_DAY_S = 24 * 60 * 60 |
83 | 65 | ||
84 | class _FetchError(Exception): | ||
85 | """Internal error thrown in _FetchHelper() when we don't want stack trace.""" | ||
86 | pass | ||
87 | |||
88 | class _CheckoutError(Exception): | ||
89 | """Internal error thrown in _CheckoutOne() when we don't want stack trace.""" | ||
90 | 66 | ||
91 | class Sync(Command, MirrorSafeCommand): | 67 | class 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 | |||
133 | credentials. | 109 | credentials. |
134 | 110 | ||
135 | By default, all projects will be synced. The --fail-fast option can be used | 111 | By default, all projects will be synced. The --fail-fast option can be used |
136 | to halt syncing as soon as possible when the the first project fails to sync. | 112 | to halt syncing as soon as possible when the first project fails to sync. |
137 | 113 | ||
138 | The --force-sync option can be used to overwrite existing git | 114 | The --force-sync option can be used to overwrite existing git |
139 | directories if they have previously been linked to a different | 115 | directories if they have previously been linked to a different |
140 | object direcotry. WARNING: This may cause data to be lost since | 116 | object directory. WARNING: This may cause data to be lost since |
141 | refs may be removed when overwriting. | 117 | refs may be removed when overwriting. |
142 | 118 | ||
143 | The --force-remove-dirty option can be used to remove previously used | 119 | The --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 | |||
191 | later is required to fix a server side protocol bug. | 167 | later 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 | |||
1001 | def _PostRepoUpgrade(manifest, quiet=False): | 1103 | def _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 | ||
1009 | def _PostRepoFetch(rp, no_repo_verify=False, verbose=False): | 1111 | |
1112 | def _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 | ||
1027 | def _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 | ||
1075 | class _FetchTimes(object): | 1144 | class _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 | |||
1131 | class PersistentTransport(xmlrpc.client.Transport): | 1196 | class 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 | ||
17 | from __future__ import print_function | ||
18 | import copy | 15 | import copy |
16 | import functools | ||
17 | import optparse | ||
19 | import re | 18 | import re |
20 | import sys | 19 | import sys |
21 | 20 | ||
22 | from command import InteractiveCommand | 21 | from command import DEFAULT_LOCAL_JOBS, InteractiveCommand |
23 | from editor import Editor | 22 | from editor import Editor |
24 | from error import HookError, UploadError | 23 | from error import UploadError |
25 | from git_command import GitCommand | 24 | from git_command import GitCommand |
26 | from project import RepoHook | 25 | from git_refs import R_HEADS |
26 | from hooks import RepoHook | ||
27 | 27 | ||
28 | from pyversion import is_python3 | ||
29 | if not is_python3(): | ||
30 | input = raw_input | ||
31 | else: | ||
32 | unicode = str | ||
33 | 28 | ||
34 | UNUSUAL_COMMIT_THRESHOLD = 5 | 29 | UNUSUAL_COMMIT_THRESHOLD = 5 |
35 | 30 | ||
31 | |||
36 | def _ConfirmManyUploads(multiple_branches=False): | 32 | def _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 | |||
47 | def _die(fmt, *args): | 44 | def _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 | |||
52 | def _SplitEmails(values): | 50 | def _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 | |||
58 | class Upload(InteractiveCommand): | 57 | class 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 | |||
126 | of the -t option to the repo command. If unset or set to "false" then | 125 | of the -t option to the repo command. If unset or set to "false" then |
127 | repo will make use of only the command line option. | 126 | repo will make use of only the command line option. |
128 | 127 | ||
128 | review.URL.uploadhashtags: | ||
129 | |||
130 | To add hashtags whenever uploading a commit, you can set a per-project | ||
131 | or global Git option to do so. The value of review.URL.uploadhashtags | ||
132 | will be used as comma delimited hashtags like the --hashtag option. | ||
133 | |||
134 | review.URL.uploadlabels: | ||
135 | |||
136 | To add labels whenever uploading a commit, you can set a per-project | ||
137 | or global Git option to do so. The value of review.URL.uploadlabels | ||
138 | will be used as comma delimited labels like the --label option. | ||
139 | |||
140 | review.URL.uploadnotify: | ||
141 | |||
142 | Control e-mail notifications when uploading. | ||
143 | https://gerrit-review.googlesource.com/Documentation/user-upload.html#notify | ||
144 | |||
129 | # References | 145 | # References |
130 | 146 | ||
131 | Gerrit Code Review: https://www.gerritcodereview.com/ | 147 | Gerrit 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 | ||
17 | from __future__ import print_function | 15 | import platform |
18 | import sys | 16 | import sys |
17 | |||
19 | from command import Command, MirrorSafeCommand | 18 | from command import Command, MirrorSafeCommand |
20 | from git_command import git, RepoSourceVersion, user_agent | 19 | from git_command import git, RepoSourceVersion, user_agent |
21 | from git_refs import HEAD | 20 | from git_refs import HEAD |
21 | from wrapper import Wrapper | ||
22 | |||
22 | 23 | ||
23 | class Version(Command, MirrorSafeCommand): | 24 | class 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 | ||
19 | from __future__ import print_function | ||
20 | |||
21 | import unittest | 17 | import unittest |
22 | 18 | ||
23 | from editor import Editor | 19 | from 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 | |||
17 | import inspect | ||
18 | import pickle | ||
19 | import unittest | ||
20 | |||
21 | import error | ||
22 | |||
23 | |||
24 | class 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 | ||
19 | from __future__ import print_function | ||
20 | |||
21 | import re | 17 | import re |
22 | import unittest | 18 | import unittest |
23 | 19 | ||
20 | try: | ||
21 | from unittest import mock | ||
22 | except ImportError: | ||
23 | import mock | ||
24 | |||
24 | import git_command | 25 | import git_command |
26 | import wrapper | ||
25 | 27 | ||
26 | 28 | ||
27 | class GitCallUnitTest(unittest.TestCase): | 29 | class 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 | |||
83 | class 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 | ||
19 | from __future__ import print_function | ||
20 | |||
21 | import os | 17 | import os |
18 | import tempfile | ||
22 | import unittest | 19 | import unittest |
23 | 20 | ||
24 | import git_config | 21 | import git_config |
25 | 22 | ||
23 | |||
26 | def fixture(*paths): | 24 | def 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 | ||
31 | class GitConfigUnitTest(unittest.TestCase): | 29 | |
32 | """Tests the GitConfig class. | 30 | class 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 | |||
127 | class 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 | |||
71 | if __name__ == '__main__': | 191 | if __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 | |||
17 | import json | ||
18 | import os | ||
19 | import platform | ||
20 | import tempfile | ||
21 | import unittest | ||
22 | from unittest import mock | ||
23 | |||
24 | import git_superproject | ||
25 | import git_trace2_event_log | ||
26 | import manifest_xml | ||
27 | import platform_utils | ||
28 | from test_manifest_xml import sort_attributes | ||
29 | |||
30 | |||
31 | class 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 | |||
375 | if __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 | |||
17 | import json | ||
18 | import os | ||
19 | import tempfile | ||
20 | import unittest | ||
21 | from unittest import mock | ||
22 | |||
23 | import git_trace2_event_log | ||
24 | |||
25 | |||
26 | class 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 | |||
328 | if __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 | |||
17 | import hooks | ||
18 | import unittest | ||
19 | |||
20 | class 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 | |||
17 | import os | ||
18 | import platform | ||
19 | import re | ||
20 | import shutil | ||
21 | import tempfile | ||
22 | import unittest | ||
23 | import xml.dom.minidom | ||
24 | |||
25 | import error | ||
26 | import manifest_xml | ||
27 | |||
28 | |||
29 | # Invalid paths that we don't want in the filesystem. | ||
30 | INVALID_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. | ||
63 | if os.path.sep != '/': | ||
64 | INVALID_FS_PATHS += tuple(x.replace('/', os.path.sep) for x in INVALID_FS_PATHS) | ||
65 | |||
66 | |||
67 | def 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 | |||
91 | class 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', '
').replace('\n', '
') | ||
126 | |||
127 | |||
128 | class 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 | |||
165 | class 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 | |||
221 | class 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 | |||
322 | class 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 | |||
423 | class 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 | |||
560 | class 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 | |||
694 | class 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 | |||
713 | class 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 | |||
730 | class 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 | |||
752 | class 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 | |||
802 | class 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 | |||
17 | import os | ||
18 | import tempfile | ||
19 | import unittest | ||
20 | |||
21 | import platform_utils | ||
22 | |||
23 | |||
24 | class RemoveTests(unittest.TestCase): | ||
25 | """Check remove() helper.""" | ||
26 | |||
27 | def testMissingOk(self): | ||
28 | """Check missing_ok handling.""" | ||
29 | with tempfile.TemporaryDirectory() as tmpdir: | ||
30 | path = os.path.join(tmpdir, 'test') | ||
31 | |||
32 | # Should not fail. | ||
33 | platform_utils.remove(path, missing_ok=True) | ||
34 | |||
35 | # Should fail. | ||
36 | self.assertRaises(OSError, platform_utils.remove, path) | ||
37 | self.assertRaises(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 | ||
19 | from __future__ import print_function | ||
20 | |||
21 | import contextlib | 17 | import contextlib |
22 | import os | 18 | import os |
23 | import shutil | 19 | import shutil |
@@ -25,7 +21,10 @@ import subprocess | |||
25 | import tempfile | 21 | import tempfile |
26 | import unittest | 22 | import unittest |
27 | 23 | ||
24 | import error | ||
25 | import git_command | ||
28 | import git_config | 26 | import git_config |
27 | import platform_utils | ||
29 | import project | 28 | import 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 | |||
45 | class 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 | ||
84 | class FakeProject(object): | 56 | class 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 | |||
111 | class 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 | |||
162 | class 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 | |||
265 | class 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 | |||
17 | import multiprocessing | ||
18 | import subprocess | ||
19 | import unittest | ||
20 | from unittest import mock | ||
21 | |||
22 | import ssh | ||
23 | |||
24 | |||
25 | class SshTests(unittest.TestCase): | ||
26 | """Tests the ssh functions.""" | ||
27 | |||
28 | def test_parse_ssh_version(self): | ||
29 | """Check _parse_ssh_version() handling.""" | ||
30 | ver = ssh._parse_ssh_version('Unknown\n') | ||
31 | self.assertEqual(ver, ()) | ||
32 | ver = ssh._parse_ssh_version('OpenSSH_1.0\n') | ||
33 | self.assertEqual(ver, (1, 0)) | ||
34 | ver = ssh._parse_ssh_version('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 | |||
17 | import optparse | ||
18 | import unittest | ||
19 | |||
20 | import subcmds | ||
21 | |||
22 | |||
23 | class 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 | |||
17 | import unittest | ||
18 | |||
19 | from subcmds import init | ||
20 | |||
21 | |||
22 | class InitCommand(unittest.TestCase): | ||
23 | """Check registered all_commands.""" | ||
24 | |||
25 | def setUp(self): | ||
26 | self.cmd = init.Init() | ||
27 | |||
28 | def test_cli_parser_good(self): | ||
29 | """Check valid command line options.""" | ||
30 | ARGV = ( | ||
31 | [], | ||
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 | ||
19 | from __future__ import print_function | 17 | import contextlib |
20 | 18 | from io import StringIO | |
21 | import os | 19 | import os |
20 | import re | ||
21 | import shutil | ||
22 | import sys | ||
23 | import tempfile | ||
22 | import unittest | 24 | import unittest |
25 | from unittest import mock | ||
23 | 26 | ||
27 | import git_command | ||
28 | import main | ||
29 | import platform_utils | ||
24 | import wrapper | 30 | import wrapper |
25 | 31 | ||
32 | |||
33 | @contextlib.contextmanager | ||
34 | def 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 | |||
26 | def fixture(*paths): | 45 | def 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 | ||
31 | class RepoWrapperUnitTest(unittest.TestCase): | 50 | |
32 | """Tests helper functions in the repo wrapper | 51 | class 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 | |||
60 | class 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 | |||
139 | class 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 | |||
171 | class 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 | |||
188 | class 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 | |||
203 | class 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 | |||
234 | class 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 | |||
259 | class 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 | |||
334 | class 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 | |||
374 | class 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 | |||
389 | class 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 | |||
420 | class 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 | |||
464 | class 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 | |||
521 | class 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 | |||
544 | class 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 | |||
79 | if __name__ == '__main__': | 570 | if __name__ == '__main__': |
80 | unittest.main() | 571 | unittest.main() |
@@ -15,8 +15,19 @@ | |||
15 | # https://tox.readthedocs.io/ | 15 | # https://tox.readthedocs.io/ |
16 | 16 | ||
17 | [tox] | 17 | [tox] |
18 | envlist = py27, py36, py37, py38 | 18 | envlist = py36, py37, py38, py39 |
19 | |||
20 | [gh-actions] | ||
21 | python = | ||
22 | 3.6: py36 | ||
23 | 3.7: py37 | ||
24 | 3.8: py38 | ||
25 | 3.9: py39 | ||
19 | 26 | ||
20 | [testenv] | 27 | [testenv] |
21 | deps = pytest | 28 | deps = pytest |
22 | commands = {toxinidir}/run_tests | 29 | commands = {envpython} run_tests |
30 | setenv = | ||
31 | GIT_AUTHOR_NAME = Repo test author | ||
32 | GIT_COMMITTER_NAME = Repo test committer | ||
33 | EMAIL = repo@gerrit.nodomain | ||
@@ -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 | ||
17 | from __future__ import print_function | ||
18 | try: | 15 | try: |
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 | |||
27 | def WrapperPath(): | 24 | def 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 | |||
31 | def Wrapper(): | 31 | def Wrapper(): |
32 | global _wrapper_module | 32 | global _wrapper_module |
33 | if not _wrapper_module: | 33 | if not _wrapper_module: |