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: |
