Compare commits

...

38 Commits
v2.0 ... v2.3

Author SHA1 Message Date
0b57eed8f0 repo: bump launcher version for accumulated fixes
Change-Id: I5d9b866cc53d3824a01f5f0af127cf0c3ff97366
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254757
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2020-02-12 23:27:11 +00:00
72b6dc8891 repo: avoid bare excepts to allow SystemExit to bubble
Bug: https://crbug.com/gerrit/12327
Change-Id: I4ce1142379b111f9ba3a2e5a437026e5c0378a9e
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254756
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2020-02-12 22:21:52 +00:00
e19d9e1a65 sync: add a "finished" message
Some people find the existing output to be a bit confusing.  It spews
a lot of git output before exiting, but it's not exactly clear what
the final state is when things pass.  Add an explicit message.

Bug: https://crbug.com/gerrit/10501
Change-Id: I9de83b595d3185feb820005b8fc81c6adc55b357
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254732
Reviewed-by: Michael Mortensen <mmortensen@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
2020-02-12 20:54:57 +00:00
8ddff5c74f repo: add --version support to the launcher
We can get version info when in a checkout, but it'd be helpful
to show that info at all times.

Change-Id: Ieeb44a503c9d7d8c487db4810bdcf3d5f6656c82
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254712
Reviewed-by: Michael Mortensen <mmortensen@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
2020-02-12 20:54:50 +00:00
8409410aa2 repo: export GIT_TRACE2_PARENT_SID
This helps with people tracing repo/git execution.  We use a similar
format to git, but a little simpler since we always initialize the
env var setting, and we want to avoid too much overhead.

Bug: https://crbug.com/gerrit/12314
Change-Id: I75675b6cc4c6f7c4f5e09f54128eba9456364d04
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254331
Reviewed-by: Josh Steadmon <steadmon@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
2020-02-12 19:57:42 +00:00
dc63181fcd flake8: Add comments in config to explain suppressed checks
Change-Id: Ib5c09b36d40a96ba9167b42b3bd2f1ed072660b7
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254611
Tested-by: David Pursehouse <dpursehouse@collab.net>
Reviewed-by: Mike Frysinger <vapier@google.com>
2020-02-12 12:33:02 +00:00
f700ac79c3 repo: move parser init out of module scope
We import the wrapper on the fly, so minimize how much code we run
in module scope.  It's pointless/wasted when importing.

Change-Id: I4a71c2030325d0a639585671cd7ebe8f22687ecd
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254072
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Reviewed-by: Mike Frysinger <vapier@google.com>
2020-02-12 11:46:30 +00:00
6f1c626a9b drop old git_require checks
We've been requiring git-1.7.2 since Oct 2012, so we can safely drop
the individual checks sprinkled throughout the code base for older.

Change-Id: I1737fff7b3f27f475960b0bff9cb300aefd5d108
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/253135
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2020-02-12 11:44:59 +00:00
77479863da flake8: Ignore 'line break before/after binary operator'
- W503 line break before binary operator
- W504 line break after binary operator

There doesn't seem to be a nice way of fixing all of these without
replacing W503 with W504 or vice-versa, or unwrapping them resulting
in excessively long lines. Let's just suppress them.

Change-Id: I7846d0124054f58e1cb480d4459cd9c86b737a50
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254608
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: David Pursehouse <dpursehouse@collab.net>
2020-02-12 07:29:25 +00:00
16a5c3ac51 git_config: Stop using backslash to wrap lines
Unwrap one unnecessarily wrapped line, and use parentheses on
a wrapped condition instead of wrapping with backslashes.

Change-Id: I12679a0547dd822b15a6551e0f6c308239ff7b2d
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254607
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: David Pursehouse <dpursehouse@collab.net>
2020-02-12 07:29:25 +00:00
145e35b805 Fix usage of bare 'except'
flake8 reports:

  E722 do not use bare 'except'

Replace them with 'except Exception' per [1] which says:

  Bare except will catch exceptions you almost certainly don't want
  to catch, including KeyboardInterrupt (the user hitting Ctrl+C) and
  Python-raised errors like SystemExit

  If you don't have a specific exception you're expecting, at least
  except Exception, which is the base type for all "Regular" exceptions.

[1] https://stackoverflow.com/a/54948581

Change-Id: Ic555ea9482645899f5b04040ddb6b24eadbf9062
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254606
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: David Pursehouse <dpursehouse@collab.net>
2020-02-12 06:49:25 +00:00
819827a42d Fix blank line issues reported by flake8
- E301 expected 1 blank line
- E302 expected 2 blank lines
- E303 too many blank lines
- E305 expected 2 blank lines after class or function definition
- E306 expected 1 blank line before a nested definition

Fixed automatically with autopep8:

  git ls-files | grep py$ | xargs autopep8 --in-place \
    --select E301,E302,E303,E305,E306

Manually fix issues in project.py caused by misuse of block comments.

Change-Id: Iee840fcaff48aae504ddac9c3e76d2acd484f6a9
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254599
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: David Pursehouse <dpursehouse@collab.net>
2020-02-12 06:36:40 +00:00
abdf750061 Fix indentation issues reported by flake8
- E121 continuation line under-indented for hanging indent
- E122 continuation line missing indentation or outdented
- E125 continuation line with same indent as next logical line
- E126 continuation line over-indented for hanging indent
- E127 continuation line over-indented for visual indent
- E128 continuation line under-indented for visual indent
- E129 visually indented line with same indent as next logical line
- E131 continuation line unaligned for hanging indent

Fixed automatically with autopep8:

  git ls-files | grep py$ | xargs autopep8 --in-place \
    --select E121,E122,E125,E126,E127,E128,E129,E131

Change-Id: Ifd95fb8e6a1a4d6e9de187b5787d64a6326dd249
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254605
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: David Pursehouse <dpursehouse@collab.net>
2020-02-12 06:36:22 +00:00
0ab95ba6d0 git_config: Unwrap unnecessarily wrapped line
Change-Id: I56806e8b9b09cd0f7fb834d7edc412682f2af1db
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254604
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: David Pursehouse <dpursehouse@collab.net>
2020-02-12 06:32:47 +00:00
5a2517f411 command: Add parentheses on wrapped condition
Surround the condition with parentheses rather than using
backslashes. This prevents confusion about indentation when
running flake8/autoflake8.

Change-Id: I01775b96f817ee616f545b55369a4864fa1d6712
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254603
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: David Pursehouse <dpursehouse@collab.net>
2020-02-12 06:32:47 +00:00
54a4e6007a Fix various whitespace issues reported by pyflakes
- E201 whitespace after '['
- E202 whitespace before '}'
- E221 multiple spaces before operator
- E222 multiple spaces after operator
- E225 missing whitespace around operator
- E226 missing whitespace around arithmetic operator
- E231 missing whitespace after ','
- E261 at least two spaces before inline comment
- E271 multiple spaces after keyword

Fixed automatically with autopep8:

  git ls-files | grep py$ | xargs autopep8 --in-place \
    --select E201,E202,E221,E222,E225,E226,E231,E261,E271

Change-Id: I367113eb8c847eb460532c7c2f8643f33040308c
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254601
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: David Pursehouse <dpursehouse@collab.net>
2020-02-12 06:00:16 +00:00
42339d7e52 Remove redundant backslashes
fleka8 reports:

  E502 the backslash is redundant between brackets

Fixed automatically with autopep8:

  git-repo $ git ls-files | grep py$ | xargs autopep8 --in-place --select E502

Change-Id: I1486ae1d17206918474363daf518274c5be8daed
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254602
Tested-by: David Pursehouse <dpursehouse@collab.net>
Reviewed-by: Mike Frysinger <vapier@google.com>
2020-02-12 05:45:44 +00:00
03ae99290a pager: Remove unnecessary semicolons
flake8 reports:

  E703 statement ends with a semicolon

Change-Id: Ia63fc9efb04425e425c0f289272db76ff1ceeb34
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254600
Tested-by: David Pursehouse <dpursehouse@collab.net>
Reviewed-by: Mike Frysinger <vapier@google.com>
2020-02-12 05:40:33 +00:00
9090e804ab Remove unused imports
flake8 reports:

  F401 'name' imported but unused

Change-Id: Id45d6efa87ddf53f2c4a0f0c4136ea361ab1b746
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254592
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: David Pursehouse <dpursehouse@collab.net>
2020-02-12 05:40:10 +00:00
eeff3537de Fix tests for membership to use 'not in'
flake8 reports:

  E713 test for membership should be 'not in'

Change-Id: I4446be67c431b7267105b53478d2ceba2af758d7
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254451
Tested-by: David Pursehouse <dpursehouse@collab.net>
Reviewed-by: Mike Frysinger <vapier@google.com>
2020-02-12 05:18:17 +00:00
8f78a83083 upload: Fix tests for object identity to use 'is not'
flake8 reports:

  E714 test for object identity should be 'is not'

Change-Id: Ib8c4100babaf952bbfe65fd56555ece8a958e4b0
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254450
Tested-by: David Pursehouse <dpursehouse@collab.net>
Reviewed-by: Mike Frysinger <vapier@google.com>
2020-02-12 05:17:49 +00:00
e5913ae410 Fix flake8 E251 unexpected spaces around keyword / parameter equals
Fixed automatically with autopep8:

  git ls-files | grep py$ | xargs autopep8 --in-place --select E251

Change-Id: I58009e1c8c91c39745d559ac919be331d4cd9e77
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254598
Tested-by: David Pursehouse <dpursehouse@collab.net>
Reviewed-by: Mike Frysinger <vapier@google.com>
2020-02-12 05:17:08 +00:00
119085e6b1 flake8: Increase max line length from 80 to 100
The Google style guide for python [1] says the maximum line length
should be 80, but there are several lines in the code base that
exceed it:

  git ls-files | grep py$ | xargs flake8 | grep E501 | wc -l
  64

I don't think it's worth going through and re-wrapping all those,
so just increase the limit to 100 which seems to be a reasonable
compromise:

  git ls-files | grep py$ | xargs flake8 | grep E501 | wc -l
  6

Leave the re-rewrapping of those lines for a follow-up commit,
though.

[1] http://google.github.io/styleguide/pyguide.html#32-line-length

Change-Id: Ia37c34301163431fd1fb4fb6697a4a482d6be077
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254595
Tested-by: David Pursehouse <dpursehouse@collab.net>
Reviewed-by: Mike Frysinger <vapier@google.com>
2020-02-12 05:16:11 +00:00
086710465e upload: Fix flake8 E241 multiple spaces after ','
Change-Id: I3a65869f9d006027270a7826d7982950c0e6759a
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254597
Tested-by: David Pursehouse <dpursehouse@collab.net>
Reviewed-by: Mike Frysinger <vapier@google.com>
2020-02-12 05:00:36 +00:00
ed4f2113d2 project: make syncing a little more self-healing
We have a few files that we optionally symlink from the work tree
.git/ to the .repo/projects/ path.  If they don't exist when we
first initialize, then we skip creating symlinks.  If the files
are created later on under the work tree .git/, repo gets upset.

This can happen with the packed-refs file: if we don't have any
packed refs initially, we don't symlink it.  But if git tries to
pack refs later on and creates the file, the project gets wedged.

We could create an empty file initially and then symlink it, but
for some files, it's not clear we want to always do that (e.g.
the .git/shallow setting).  Instead, lets make handling of these
paths more dynamic.  If they show up later on in the work tree
.git/ only, we'll take care of relocating & symlinking.  This
also makes repo a little more robust and autorecovers incase a
path goes missing in one of the dirs.

Ideally we wouldn't monkey around at all here, but considering
the only option we give to users currently is to blow things
away with --force-sync, this seems a bit better.

Bug: https://crbug.com/gerrit/12324
Change-Id: Ia6960f1896ac6d890c762d7d053684a1c6ab2c87
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254632
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2020-02-12 04:48:36 +00:00
719675bcec info: Fix formatting of block comment
flake8 reports:

  E265 block comment should start with '# '

While we're at it, add a period at the end of the comment sentence.

Change-Id: Icb7119079a1d64e6defafc3f6d24e99dbf16139d
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254596
Tested-by: David Pursehouse <dpursehouse@collab.net>
Reviewed-by: Mike Frysinger <vapier@google.com>
2020-02-12 04:31:40 +00:00
21c1575ee4 upload: add a --ignore-hooks option
When upload hooks fail, people are forced to use --no-verify to upload
CLs anyways.  When projects have flaky hooks, this trains people to
always use that option.  This is obviously bad: hooks might get fixed,
or some of the hooks are always good & people should review.

Lets add an --ignore-hooks option.  This still runs the hooks, but any
failures will be ignored and allow the user to upload anyways.

Bug: https://crbug.com/gerrit/12230
Change-Id: Ide2ac8a40a656bfcd6aae20c3ce8118e06bf909b
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254452
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
2020-02-12 04:18:49 +00:00
8f9e02231a Remove trailing blank lines
flake8 reports:

  W391 blank line at end of file

Change-Id: I5498b2de2d1268d4f1f4b9e1760f9fa93a6da4cd
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254594
Tested-by: David Pursehouse <dpursehouse@collab.net>
Reviewed-by: Mike Frysinger <vapier@google.com>
2020-02-12 02:58:17 +00:00
348e218d5b test_project.py: Remove unused variable in 'with' statement
flake8 reports:

 F841 local variable 'f' is assigned to but never used

Change-Id: If808eb381ee44c7da71e6281615a06a6723cf945
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254593
Tested-by: David Pursehouse <dpursehouse@collab.net>
Reviewed-by: Mike Frysinger <vapier@google.com>
2020-02-12 02:56:32 +00:00
4bbba7d627 Fix duplicate method name in test_project.py
flake8 reports:

  F811 redefinition of unused 'test_src_block_dir' from line 259

which is caused by having two methods with the same name. Rename
them both to better desribe their purpose.

Change-Id: If7612a42001776d71bb1a6a80fc631d3d262e6ce
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254449
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: David Pursehouse <dpursehouse@collab.net>
2020-02-12 02:55:51 +00:00
dc1d0e0c7f Revert "Save cookies back to jar when fetching clone.bundle"
This reverts commit 4abf8e6ef8.

The curl process for updating the cookie file is not atomic.  When
fetching many bundles in parallel, we can sometimes corrupt the file
causing it to be cleared.  Since users should manage gitcookies on
their own, leave it read-only.

Bug: https://crbug.com/gerrit/12300
Change-Id: Id472c99b197bc4cf8533c649f8881509f38643c1
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254092
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2020-02-12 02:00:16 +00:00
82caef67a1 repo: lower min version of git a bit
We were perhaps a bit too hasty to jump to git-2.10.  Existing LTS
releases of Ubuntu are quite old still: Trusty has 1.9 while Xenial
has 2.5.  While we plan on dropping support for those eventually as
we migrate to Python 3.6, we don't need to be so strict just yet on
the git versions.

We also want to disconnect the version the repo launcher requires
from the version the rest of the source tree requires.  The repo
launcher doesn't need as many features, and being flexible there
allows us more freedom to upgrade & rollback as needed.

So we'll allow git-1.7 again, but start warning on any users older
than git-1.9.  This aligns better with existing LTS releases, and
gives users a chance to start upgrading before we cut them off.

Change-Id: I140305dd8e42c9719c84e2aee0dc6a5c5b18da25
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254573
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2020-02-12 00:28:03 +00:00
3645bd2420 docs: document git/python/Ubuntu/Debian release schedules
Going purely on upstream package release cycles doesn't tell the whole
story: a lot of people run LTS distros which will have older versions
of software we want to support.

Build out a table for us to quickly reference when making decisions as
to what versions of git/python we want to support, and when we can drop
them.  This will also help to refer users to as why we made a specific
decision that might be affecting them.

Change-Id: I7aea24bbefd50e358aeacf11e8c15a346c8fb8a9
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254572
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
2020-02-12 00:27:59 +00:00
5f2b045195 sync: change how we preserve objects in shared repos
Some automatic git operations will prune objects on us, and not just
the gc step.  Normally we don't care, but with shared projects, we
will have multiple git checkouts with refs that the others cannot
see, but with a shared object dir.  Any pruning of objects based on
refs in just one repo can easily break the others.

git-2.7.0 introduced a preciousObjects setting which tells git to
never prune objects for this exact scenario: there might be refs in
some location that git is unable to see.

Change-Id: I781de27c5bbe1d4c70f0187566141c9cce088bd8
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254392
Reviewed-by: Nasser Grainawi <nasser@codeaurora.org>
Reviewed-by: David Riley <davidriley@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
2020-02-11 23:58:43 +00:00
163d42eb43 project: fix bytes/str encoding when updating git submodules
Since tempfile.mkstemp() returns a file handle in binary mode,
make sure we turn our strings into bytes before writing.

Bug: https://crbug.com/gerrit/12043
Change-Id: I3e84d595e84b8bc12a1fbc7fd0bb3ea0ba2832b0
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254393
Reviewed-by: Michael Mortensen <mmortensen@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
2020-02-11 18:49:47 +00:00
07392ed326 project: allow src=. with symlinks
Some Android/Nest manifests are using <linkfile> with src="." to
create stable paths to specific projects.  Allow that specific
use case as it seems reasonable to support.

Bug: https://crbug.com/gerrit/11218
Change-Id: I16dbe8d9fe42ea45440afcb61404c753bff1930d
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254330
Reviewed-by: Chanho Park <parkch98@gmail.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
2020-02-11 04:23:26 +00:00
3285e4b436 main: rework launcher version checking
The code has an ad-hoc check in that it requires the launcher major
version to not be less than the source code version.  We don't really
care about that requirement, and it doesn't fit with our other version
checks.  Rework it so we explicitly declare the min launcher version
that is supported.

We'll start with requiring repo launcher 1.15 which was released back
in 2012.  Hopefully no one has anything older than that, although it's
not clear we work with even newer versions than that :).  But let's be
a little conservative with the first update to this logic.

Bug: https://crbug.com/gerrit/10418
Change-Id: I611d70c60324d313c76874e978b8499a491a5d00
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254278
Reviewed-by: Michael Mortensen <mmortensen@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
2020-02-10 23:20:55 +00:00
ae62541005 manifest_xml: allow src=. with symlinks
Some Android/Nest manifests are using <linkfile> with src="." to
create stable paths to specific projects.  Allow that specific
use case as it seems reasonable to support.

Bug: https://crbug.com/gerrit/11218
Change-Id: I5eadec257cd58ba0f8687c590ddc250a7a414a85
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254276
Reviewed-by: Michael Mortensen <mmortensen@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
2020-02-10 23:19:31 +00:00
52 changed files with 865 additions and 434 deletions

14
.flake8
View File

@ -1,3 +1,13 @@
[flake8] [flake8]
max-line-length=80 max-line-length=100
ignore=E111,E114,E402 ignore=
# E111: Indentation is not a multiple of four
E111,
# E114: Indentation is not a multiple of four (comment)
E114,
# E402: Module level import not at top of file
E402,
# W503: Line break before binary operator
W503,
# W504: Line break after binary operator
W504

View File

@ -84,6 +84,7 @@ def _Color(fg=None, bg=None, attr=None):
code = '' code = ''
return code return code
DEFAULT = None DEFAULT = None

View File

@ -123,9 +123,9 @@ class Command(object):
project = None project = None
if os.path.exists(path): if os.path.exists(path):
oldpath = None oldpath = None
while path and \ while (path and
path != oldpath and \ path != oldpath and
path != manifest.topdir: path != manifest.topdir):
try: try:
project = self._by_path[path] project = self._by_path[path]
break break
@ -236,6 +236,7 @@ class InteractiveCommand(Command):
"""Command which requires user interaction on the tty and """Command which requires user interaction on the tty and
must not run within a pager, even if the user asks to. must not run within a pager, even if the user asks to.
""" """
def WantPager(self, _opt): def WantPager(self, _opt):
return False return False
@ -244,6 +245,7 @@ class PagedCommand(Command):
"""Command which defaults to output in a pager, as its """Command which defaults to output in a pager, as its
display tends to be larger than one screen full. display tends to be larger than one screen full.
""" """
def WantPager(self, _opt): def WantPager(self, _opt):
return True return True

View File

@ -161,7 +161,91 @@ You can create a short changelog using the command:
$ git log --format="%h (%aN) %s" --no-merges origin/stable..$r $ git log --format="%h (%aN) %s" --no-merges origin/stable..$r
``` ```
## Project References
Here's a table showing the relationship of major tools, their EOL dates, and
their status in Ubuntu & Debian.
Those distros tend to be good indicators of how long we need to support things.
Things in bold indicate stuff to take note of, but does not guarantee that we
still support them.
Things in italics are things we used to care about but probably don't anymore.
| Date | EOL | [Git][rel-g] | [Python][rel-p] | [Ubuntu][rel-u] / [Debian][rel-d] | Git | Python |
|:--------:|:------------:|--------------|-----------------|-----------------------------------|-----|--------|
| Oct 2008 | *Oct 2013* | | 2.6.0 | *10.04 Lucid* - 10.10 Maverick / *Squeeze* |
| Dec 2008 | *Feb 2009* | | 3.0.0 |
| Feb 2009 | *Mar 2012* | | | Debian 5 Lenny | 1.5.6.5 | 2.5.2 |
| Jun 2009 | *Jun 2016* | | 3.1.0 | *10.04 Lucid* - 10.10 Maverick / *Squeeze* |
| Feb 2010 | *Oct 2012* | 1.7.0 | | *10.04 Lucid* - *12.04 Precise* - 12.10 Quantal |
| Apr 2010 | *Apr 2015* | | | *10.04 Lucid* | 1.7.0.4 | 2.6.5 3.1.2 |
| Jul 2010 | *Dec 2019* | | **2.7.0** | 11.04 Natty - **<current>** |
| Oct 2010 | | | | 10.10 Maverick | 1.7.1 | 2.6.6 3.1.3 |
| Feb 2011 | *Feb 2016* | | | Debian 6 Squeeze | 1.7.2.5 | 2.6.6 3.1.3 |
| Apr 2011 | | | | 11.04 Natty | 1.7.4 | 2.7.1 3.2.0 |
| Oct 2011 | *Feb 2016* | | 3.2.0 | 11.04 Natty - 12.10 Quantal |
| Oct 2011 | | | | 11.10 Ocelot | 1.7.5.4 | 2.7.2 3.2.2 |
| Apr 2012 | *Apr 2019* | | | *12.04 Precise* | 1.7.9.5 | 2.7.3 3.2.3 |
| Sep 2012 | *Sep 2017* | | 3.3.0 | 13.04 Raring - 13.10 Saucy |
| Oct 2012 | *Dec 2014* | 1.8.0 | | 13.04 Raring - 13.10 Saucy |
| Oct 2012 | | | | 12.10 Quantal | 1.7.10.4 | 2.7.3 3.2.3 |
| Apr 2013 | | | | 13.04 Raring | 1.8.1.2 | 2.7.4 3.3.1 |
| May 2013 | *May 2018* | | | Debian 7 Wheezy | 1.7.10.4 | 2.7.3 3.2.3 |
| Oct 2013 | | | | 13.10 Saucy | 1.8.3.2 | 2.7.5 3.3.2 |
| Feb 2014 | *Dec 2014* | **1.9.0** | | **14.04 Trusty** |
| Mar 2014 | *Mar 2019* | | **3.4.0** | **14.04 Trusty** - 15.10 Wily / **Jessie** |
| Apr 2014 | **Apr 2022** | | | **14.04 Trusty** | 1.9.1 | 2.7.5 3.4.0 |
| May 2014 | *Dec 2014* | 2.0.0 |
| Aug 2014 | *Dec 2014* | **2.1.0** | | 14.10 Utopic - 15.04 Vivid / **Jessie** |
| Oct 2014 | | | | 14.10 Utopic | 2.1.0 | 2.7.8 3.4.2 |
| Nov 2014 | *Sep 2015* | 2.2.0 |
| Feb 2015 | *Sep 2015* | 2.3.0 |
| Apr 2015 | *May 2017* | 2.4.0 |
| Apr 2015 | **Jun 2020** | | | **Debian 8 Jessie** | 2.1.4 | 2.7.9 3.4.2 |
| Apr 2015 | | | | 15.04 Vivid | 2.1.4 | 2.7.9 3.4.3 |
| Jul 2015 | *May 2017* | 2.5.0 | | 15.10 Wily |
| Sep 2015 | *May 2017* | 2.6.0 |
| Sep 2015 | **Sep 2020** | | **3.5.0** | **16.04 Xenial** - 17.04 Zesty / **Stretch** |
| Oct 2015 | | | | 15.10 Wily | 2.5.0 | 2.7.9 3.4.3 |
| Jan 2016 | *Jul 2017* | **2.7.0** | | **16.04 Xenial** |
| Mar 2016 | *Jul 2017* | 2.8.0 |
| Apr 2016 | **Apr 2024** | | | **16.04 Xenial** | 2.7.4 | 2.7.11 3.5.1 |
| Jun 2016 | *Jul 2017* | 2.9.0 | | 16.10 Yakkety |
| Sep 2016 | *Sep 2017* | 2.10.0 |
| Oct 2016 | | | | 16.10 Yakkety | 2.9.3 | 2.7.11 3.5.1 |
| Nov 2016 | *Sep 2017* | **2.11.0** | | 17.04 Zesty / **Stretch** |
| Dec 2016 | **Dec 2021** | | **3.6.0** | 17.10 Artful - **18.04 Bionic** - 18.10 Cosmic |
| Feb 2017 | *Sep 2017* | 2.12.0 |
| Apr 2017 | | | | 17.04 Zesty | 2.11.0 | 2.7.13 3.5.3 |
| May 2017 | *May 2018* | 2.13.0 |
| Jun 2017 | **Jun 2022** | | | **Debian 9 Stretch** | 2.11.0 | 2.7.13 3.5.3 |
| Aug 2017 | *Dec 2019* | 2.14.0 | | 17.10 Artful |
| Oct 2017 | *Dec 2019* | 2.15.0 |
| Oct 2017 | | | | 17.10 Artful | 2.14.1 | 2.7.14 3.6.3 |
| Jan 2018 | *Dec 2019* | 2.16.0 |
| Apr 2018 | *Dec 2019* | 2.17.0 | | **18.04 Bionic** |
| Apr 2018 | **Apr 2028** | | | **18.04 Bionic** | 2.17.0 | 2.7.15 3.6.5 |
| Jun 2018 | *Dec 2019* | 2.18.0 |
| Jun 2018 | **Jun 2023** | | 3.7.0 | 19.04 Disco - **20.04 Focal** / **Buster** |
| Sep 2018 | *Dec 2019* | 2.19.0 | | 18.10 Cosmic |
| Oct 2018 | | | | 18.10 Cosmic | 2.19.1 | 2.7.15 3.6.6 |
| Dec 2018 | *Dec 2019* | **2.20.0** | | 19.04 Disco / **Buster** |
| Feb 2019 | *Dec 2019* | 2.21.0 |
| Apr 2019 | | | | 19.04 Disco | 2.20.1 | 2.7.16 3.7.3 |
| Jun 2019 | | 2.22.0 |
| Jul 2019 | **Jul 2024** | | | **Debian 10 Buster** | 2.20.1 | 2.7.16 3.7.3 |
| Aug 2019 | | 2.23.0 |
| Oct 2019 | **Oct 2024** | | 3.8.0 |
| Oct 2019 | | | | 19.10 Eoan | 2.20.1 | 2.7.17 3.7.5 |
| Nov 2019 | | 2.24.0 |
| Jan 2020 | | 2.25.0 | | **20.04 Focal** |
| Apr 2020 | **Apr 2030** | | | **20.04 Focal** | 2.25.0 | 2.7.17 3.7.5 |
[rel-d]: https://en.wikipedia.org/wiki/Debian_version_history
[rel-g]: https://en.wikipedia.org/wiki/Git#Releases
[rel-p]: https://en.wikipedia.org/wiki/History_of_Python#Table_of_versions
[rel-u]: https://en.wikipedia.org/wiki/Ubuntu_version_history#Table_of_versions
[example announcement]: https://groups.google.com/d/topic/repo-discuss/UGBNismWo1M/discussion [example announcement]: https://groups.google.com/d/topic/repo-discuss/UGBNismWo1M/discussion
[repo-discuss@googlegroups.com]: https://groups.google.com/forum/#!forum/repo-discuss [repo-discuss@googlegroups.com]: https://groups.google.com/forum/#!forum/repo-discuss
[go/repo-release]: https://goto.google.com/repo-release [go/repo-release]: https://goto.google.com/repo-release

View File

@ -24,6 +24,7 @@ import tempfile
from error import EditorError from error import EditorError
import platform_utils import platform_utils
class Editor(object): class Editor(object):
"""Manages the user's preferred text editor.""" """Manages the user's preferred text editor."""
@ -57,7 +58,7 @@ class Editor(object):
if os.getenv('TERM') == 'dumb': if os.getenv('TERM') == 'dumb':
print( print(
"""No editor specified in GIT_EDITOR, core.editor, VISUAL or EDITOR. """No editor specified in GIT_EDITOR, core.editor, VISUAL or EDITOR.
Tried to fall back to vi but terminal is dumb. Please configure at Tried to fall back to vi but terminal is dumb. Please configure at
least one of these before using this command.""", file=sys.stderr) least one of these before using this command.""", file=sys.stderr)
sys.exit(1) sys.exit(1)
@ -104,10 +105,10 @@ least one of these before using this command.""", file=sys.stderr)
rc = subprocess.Popen(args, shell=shell).wait() rc = subprocess.Popen(args, shell=shell).wait()
except OSError as e: except OSError as e:
raise EditorError('editor failed, %s: %s %s' raise EditorError('editor failed, %s: %s %s'
% (str(e), editor, path)) % (str(e), editor, path))
if rc != 0: if rc != 0:
raise EditorError('editor failed with exit status %d: %s %s' raise EditorError('editor failed with exit status %d: %s %s'
% (rc, editor, path)) % (rc, editor, path))
with open(path, mode='rb') as fd2: with open(path, mode='rb') as fd2:
return fd2.read().decode('utf-8') return fd2.read().decode('utf-8')

View File

@ -14,21 +14,26 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
class ManifestParseError(Exception): class ManifestParseError(Exception):
"""Failed to parse the manifest file. """Failed to parse the manifest file.
""" """
class ManifestInvalidRevisionError(Exception): class ManifestInvalidRevisionError(Exception):
"""The revision value in a project is incorrect. """The revision value in a project is incorrect.
""" """
class ManifestInvalidPathError(Exception): class ManifestInvalidPathError(Exception):
"""A path used in <copyfile> or <linkfile> is incorrect. """A path used in <copyfile> or <linkfile> is incorrect.
""" """
class NoManifestException(Exception): class NoManifestException(Exception):
"""The required manifest does not exist. """The required manifest does not exist.
""" """
def __init__(self, path, reason): def __init__(self, path, reason):
super(NoManifestException, self).__init__() super(NoManifestException, self).__init__()
self.path = path self.path = path
@ -37,9 +42,11 @@ class NoManifestException(Exception):
def __str__(self): def __str__(self):
return self.reason return self.reason
class EditorError(Exception): class EditorError(Exception):
"""Unspecified error from the user's text editor. """Unspecified error from the user's text editor.
""" """
def __init__(self, reason): def __init__(self, reason):
super(EditorError, self).__init__() super(EditorError, self).__init__()
self.reason = reason self.reason = reason
@ -47,9 +54,11 @@ class EditorError(Exception):
def __str__(self): def __str__(self):
return self.reason return self.reason
class GitError(Exception): class GitError(Exception):
"""Unspecified internal error from git. """Unspecified internal error from git.
""" """
def __init__(self, command): def __init__(self, command):
super(GitError, self).__init__() super(GitError, self).__init__()
self.command = command self.command = command
@ -57,9 +66,11 @@ class GitError(Exception):
def __str__(self): def __str__(self):
return self.command return self.command
class UploadError(Exception): class UploadError(Exception):
"""A bundle upload to Gerrit did not succeed. """A bundle upload to Gerrit did not succeed.
""" """
def __init__(self, reason): def __init__(self, reason):
super(UploadError, self).__init__() super(UploadError, self).__init__()
self.reason = reason self.reason = reason
@ -67,9 +78,11 @@ class UploadError(Exception):
def __str__(self): def __str__(self):
return self.reason return self.reason
class DownloadError(Exception): class DownloadError(Exception):
"""Cannot download a repository. """Cannot download a repository.
""" """
def __init__(self, reason): def __init__(self, reason):
super(DownloadError, self).__init__() super(DownloadError, self).__init__()
self.reason = reason self.reason = reason
@ -77,9 +90,11 @@ class DownloadError(Exception):
def __str__(self): def __str__(self):
return self.reason return self.reason
class NoSuchProjectError(Exception): class NoSuchProjectError(Exception):
"""A specified project does not exist in the work tree. """A specified project does not exist in the work tree.
""" """
def __init__(self, name=None): def __init__(self, name=None):
super(NoSuchProjectError, self).__init__() super(NoSuchProjectError, self).__init__()
self.name = name self.name = name
@ -93,6 +108,7 @@ class NoSuchProjectError(Exception):
class InvalidProjectGroupsError(Exception): class InvalidProjectGroupsError(Exception):
"""A specified project is not suitable for the specified groups """A specified project is not suitable for the specified groups
""" """
def __init__(self, name=None): def __init__(self, name=None):
super(InvalidProjectGroupsError, self).__init__() super(InvalidProjectGroupsError, self).__init__()
self.name = name self.name = name
@ -102,15 +118,18 @@ class InvalidProjectGroupsError(Exception):
return 'in current directory' return 'in current directory'
return self.name return self.name
class RepoChangedException(Exception): class RepoChangedException(Exception):
"""Thrown if 'repo sync' results in repo updating its internal """Thrown if 'repo sync' results in repo updating its internal
repo or manifest repositories. In this special case we must repo or manifest repositories. In this special case we must
use exec to re-execute repo with the new code and manifest. use exec to re-execute repo with the new code and manifest.
""" """
def __init__(self, extra_args=None): def __init__(self, extra_args=None):
super(RepoChangedException, self).__init__() super(RepoChangedException, self).__init__()
self.extra_args = extra_args or [] self.extra_args = extra_args or []
class HookError(Exception): class HookError(Exception):
"""Thrown if a 'repo-hook' could not be run. """Thrown if a 'repo-hook' could not be run.

View File

@ -23,6 +23,7 @@ TASK_COMMAND = 'command'
TASK_SYNC_NETWORK = 'sync-network' TASK_SYNC_NETWORK = 'sync-network'
TASK_SYNC_LOCAL = 'sync-local' TASK_SYNC_LOCAL = 'sync-local'
class EventLog(object): class EventLog(object):
"""Event log that records events that occurred during a repo invocation. """Event log that records events that occurred during a repo invocation.
@ -138,7 +139,7 @@ class EventLog(object):
Returns: Returns:
A dictionary of the event added to the log. A dictionary of the event added to the log.
""" """
event['status'] = self.GetStatusString(success) event['status'] = self.GetStatusString(success)
event['finish_time'] = finish event['finish_time'] = finish
return event return event
@ -165,6 +166,7 @@ class EventLog(object):
# An integer id that is unique across this invocation of the program. # An integer id that is unique across this invocation of the program.
_EVENT_ID = multiprocessing.Value('i', 1) _EVENT_ID = multiprocessing.Value('i', 1)
def _NextEventId(): def _NextEventId():
"""Helper function for grabbing the next unique id. """Helper function for grabbing the next unique id.

View File

@ -28,8 +28,17 @@ from repo_trace import REPO_TRACE, IsTrace, Trace
from wrapper import Wrapper from wrapper import Wrapper
GIT = 'git' GIT = 'git'
# Should keep in sync with the "repo" launcher file. # NB: These do not need to be kept in sync with the repo launcher script.
MIN_GIT_VERSION = (2, 10, 2) # These may be much newer as it allows the repo launcher to roll between
# different repo releases while source versions might require a newer git.
#
# The soft version is when we start warning users that the version is old and
# we'll be dropping support for it. We'll refuse to work with versions older
# than the hard version.
#
# git-1.7 is in (EOL) Ubuntu Precise. git-1.9 is in Ubuntu Trusty.
MIN_GIT_VERSION_SOFT = (1, 9, 1)
MIN_GIT_VERSION_HARD = (1, 7, 2)
GIT_DIR = 'GIT_DIR' GIT_DIR = 'GIT_DIR'
LAST_GITDIR = None LAST_GITDIR = None
@ -39,6 +48,7 @@ _ssh_proxy_path = None
_ssh_sock_path = None _ssh_sock_path = None
_ssh_clients = [] _ssh_clients = []
def ssh_sock(create=True): def ssh_sock(create=True):
global _ssh_sock_path global _ssh_sock_path
if _ssh_sock_path is None: if _ssh_sock_path is None:
@ -48,27 +58,31 @@ def ssh_sock(create=True):
if not os.path.exists(tmp_dir): if not os.path.exists(tmp_dir):
tmp_dir = tempfile.gettempdir() tmp_dir = tempfile.gettempdir()
_ssh_sock_path = os.path.join( _ssh_sock_path = os.path.join(
tempfile.mkdtemp('', 'ssh-', tmp_dir), tempfile.mkdtemp('', 'ssh-', tmp_dir),
'master-%r@%h:%p') 'master-%r@%h:%p')
return _ssh_sock_path return _ssh_sock_path
def _ssh_proxy(): def _ssh_proxy():
global _ssh_proxy_path global _ssh_proxy_path
if _ssh_proxy_path is None: if _ssh_proxy_path is None:
_ssh_proxy_path = os.path.join( _ssh_proxy_path = os.path.join(
os.path.dirname(__file__), os.path.dirname(__file__),
'git_ssh') 'git_ssh')
return _ssh_proxy_path return _ssh_proxy_path
def _add_ssh_client(p): def _add_ssh_client(p):
_ssh_clients.append(p) _ssh_clients.append(p)
def _remove_ssh_client(p): def _remove_ssh_client(p):
try: try:
_ssh_clients.remove(p) _ssh_clients.remove(p)
except ValueError: except ValueError:
pass pass
def terminate_ssh_clients(): def terminate_ssh_clients():
global _ssh_clients global _ssh_clients
for p in _ssh_clients: for p in _ssh_clients:
@ -79,8 +93,10 @@ def terminate_ssh_clients():
pass pass
_ssh_clients = [] _ssh_clients = []
_git_version = None _git_version = None
class _GitCall(object): class _GitCall(object):
def version_tuple(self): def version_tuple(self):
global _git_version global _git_version
@ -92,12 +108,15 @@ class _GitCall(object):
return _git_version return _git_version
def __getattr__(self, name): def __getattr__(self, name):
name = name.replace('_','-') name = name.replace('_', '-')
def fun(*cmdv): def fun(*cmdv):
command = [name] command = [name]
command.extend(cmdv) command.extend(cmdv)
return GitCommand(None, command).Wait() == 0 return GitCommand(None, command).Wait() == 0
return fun return fun
git = _GitCall() git = _GitCall()
@ -178,8 +197,10 @@ class UserAgent(object):
return self._git_ua return self._git_ua
user_agent = UserAgent() user_agent = UserAgent()
def git_require(min_version, fail=False, msg=''): def git_require(min_version, fail=False, msg=''):
git_version = git.version_tuple() git_version = git.version_tuple()
if min_version <= git_version: if min_version <= git_version:
@ -192,21 +213,23 @@ def git_require(min_version, fail=False, msg=''):
sys.exit(1) sys.exit(1)
return False return False
def _setenv(env, name, value): def _setenv(env, name, value):
env[name] = value.encode() env[name] = value.encode()
class GitCommand(object): class GitCommand(object):
def __init__(self, def __init__(self,
project, project,
cmdv, cmdv,
bare = False, bare=False,
provide_stdin = False, provide_stdin=False,
capture_stdout = False, capture_stdout=False,
capture_stderr = False, capture_stderr=False,
disable_editor = False, disable_editor=False,
ssh_proxy = False, ssh_proxy=False,
cwd = None, cwd=None,
gitdir = None): gitdir=None):
env = self._GetBasicEnv() env = self._GetBasicEnv()
# If we are not capturing std* then need to print it. # If we are not capturing std* then need to print it.
@ -286,11 +309,11 @@ class GitCommand(object):
try: try:
p = subprocess.Popen(command, p = subprocess.Popen(command,
cwd = cwd, cwd=cwd,
env = env, env=env,
stdin = stdin, stdin=stdin,
stdout = stdout, stdout=stdout,
stderr = stderr) stderr=stderr)
except Exception as e: except Exception as e:
raise GitError('%s: %s' % (command[1], e)) raise GitError('%s: %s' % (command[1], e))

View File

@ -59,39 +59,45 @@ ID_RE = re.compile(r'^[0-9a-f]{40}$')
REVIEW_CACHE = dict() REVIEW_CACHE = dict()
def IsChange(rev): def IsChange(rev):
return rev.startswith(R_CHANGES) return rev.startswith(R_CHANGES)
def IsId(rev): def IsId(rev):
return ID_RE.match(rev) return ID_RE.match(rev)
def IsTag(rev): def IsTag(rev):
return rev.startswith(R_TAGS) return rev.startswith(R_TAGS)
def IsImmutable(rev): def IsImmutable(rev):
return IsChange(rev) or IsId(rev) or IsTag(rev) return IsChange(rev) or IsId(rev) or IsTag(rev)
def _key(name): def _key(name):
parts = name.split('.') parts = name.split('.')
if len(parts) < 2: if len(parts) < 2:
return name.lower() return name.lower()
parts[ 0] = parts[ 0].lower() parts[0] = parts[0].lower()
parts[-1] = parts[-1].lower() parts[-1] = parts[-1].lower()
return '.'.join(parts) return '.'.join(parts)
class GitConfig(object): class GitConfig(object):
_ForUser = None _ForUser = None
@classmethod @classmethod
def ForUser(cls): def ForUser(cls):
if cls._ForUser is None: if cls._ForUser is None:
cls._ForUser = cls(configfile = os.path.expanduser('~/.gitconfig')) cls._ForUser = cls(configfile=os.path.expanduser('~/.gitconfig'))
return cls._ForUser return cls._ForUser
@classmethod @classmethod
def ForRepository(cls, gitdir, defaults=None): def ForRepository(cls, gitdir, defaults=None):
return cls(configfile = os.path.join(gitdir, 'config'), return cls(configfile=os.path.join(gitdir, 'config'),
defaults = defaults) defaults=defaults)
def __init__(self, configfile, defaults=None, jsonFile=None): def __init__(self, configfile, defaults=None, jsonFile=None):
self.file = configfile self.file = configfile
@ -104,16 +110,16 @@ class GitConfig(object):
self._json = jsonFile self._json = jsonFile
if self._json is None: if self._json is None:
self._json = os.path.join( self._json = os.path.join(
os.path.dirname(self.file), os.path.dirname(self.file),
'.repo_' + os.path.basename(self.file) + '.json') '.repo_' + os.path.basename(self.file) + '.json')
def Has(self, name, include_defaults = True): def Has(self, name, include_defaults=True):
"""Return true if this configuration file has the key. """Return true if this configuration file has the key.
""" """
if _key(name) in self._cache: if _key(name) in self._cache:
return True return True
if include_defaults and self.defaults: if include_defaults and self.defaults:
return self.defaults.Has(name, include_defaults = True) return self.defaults.Has(name, include_defaults=True)
return False return False
def GetBoolean(self, name): def GetBoolean(self, name):
@ -142,7 +148,7 @@ class GitConfig(object):
v = self._cache[_key(name)] v = self._cache[_key(name)]
except KeyError: except KeyError:
if self.defaults: if self.defaults:
return self.defaults.GetString(name, all_keys = all_keys) return self.defaults.GetString(name, all_keys=all_keys)
v = [] v = []
if not all_keys: if not all_keys:
@ -153,7 +159,7 @@ class GitConfig(object):
r = [] r = []
r.extend(v) r.extend(v)
if self.defaults: if self.defaults:
r.extend(self.defaults.GetString(name, all_keys = True)) r.extend(self.defaults.GetString(name, all_keys=True))
return r return r
def SetString(self, name, value): def SetString(self, name, value):
@ -217,7 +223,7 @@ class GitConfig(object):
""" """
return self._sections.get(section, set()) return self._sections.get(section, set())
def HasSection(self, section, subsection = ''): def HasSection(self, section, subsection=''):
"""Does at least one key in section.subsection exist? """Does at least one key in section.subsection exist?
""" """
try: try:
@ -268,8 +274,7 @@ class GitConfig(object):
def _ReadJson(self): def _ReadJson(self):
try: try:
if os.path.getmtime(self._json) \ if os.path.getmtime(self._json) <= os.path.getmtime(self.file):
<= os.path.getmtime(self.file):
platform_utils.remove(self._json) platform_utils.remove(self._json)
return None return None
except OSError: except OSError:
@ -323,8 +328,8 @@ class GitConfig(object):
p = GitCommand(None, p = GitCommand(None,
command, command,
capture_stdout = True, capture_stdout=True,
capture_stderr = True) capture_stderr=True)
if p.Wait() == 0: if p.Wait() == 0:
return p.stdout return p.stdout
else: else:
@ -392,6 +397,7 @@ _master_keys = set()
_ssh_master = True _ssh_master = True
_master_keys_lock = None _master_keys_lock = None
def init_ssh(): def init_ssh():
"""Should be called once at the start of repo to init ssh master handling. """Should be called once at the start of repo to init ssh master handling.
@ -401,6 +407,7 @@ def init_ssh():
assert _master_keys_lock is None, "Should only call init_ssh once" assert _master_keys_lock is None, "Should only call init_ssh once"
_master_keys_lock = _threading.Lock() _master_keys_lock = _threading.Lock()
def _open_ssh(host, port=None): def _open_ssh(host, port=None):
global _ssh_master global _ssh_master
@ -421,17 +428,17 @@ def _open_ssh(host, port=None):
if key in _master_keys: if key in _master_keys:
return True return True
if not _ssh_master \ if (not _ssh_master
or 'GIT_SSH' in os.environ \ or 'GIT_SSH' in os.environ
or sys.platform in ('win32', 'cygwin'): or sys.platform in ('win32', 'cygwin')):
# failed earlier, or cygwin ssh can't do this # failed earlier, or cygwin ssh can't do this
# #
return False return False
# We will make two calls to ssh; this is the common part of both calls. # We will make two calls to ssh; this is the common part of both calls.
command_base = ['ssh', command_base = ['ssh',
'-o','ControlPath %s' % ssh_sock(), '-o', 'ControlPath %s' % ssh_sock(),
host] host]
if port is not None: if port is not None:
command_base[1:1] = ['-p', str(port)] command_base[1:1] = ['-p', str(port)]
@ -439,13 +446,13 @@ def _open_ssh(host, port=None):
# ...but before actually starting a master, we'll double-check. This can # ...but before actually starting a master, we'll double-check. This can
# be important because we can't tell that that 'git@myhost.com' is the same # be important because we can't tell that that 'git@myhost.com' is the same
# as 'myhost.com' where "User git" is setup in the user's ~/.ssh/config file. # as 'myhost.com' where "User git" is setup in the user's ~/.ssh/config file.
check_command = command_base + ['-O','check'] check_command = command_base + ['-O', 'check']
try: try:
Trace(': %s', ' '.join(check_command)) Trace(': %s', ' '.join(check_command))
check_process = subprocess.Popen(check_command, check_process = subprocess.Popen(check_command,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE) stderr=subprocess.PIPE)
check_process.communicate() # read output, but ignore it... check_process.communicate() # read output, but ignore it...
isnt_running = check_process.wait() isnt_running = check_process.wait()
if not isnt_running: if not isnt_running:
@ -458,16 +465,14 @@ def _open_ssh(host, port=None):
# to the log there. # to the log there.
pass pass
command = command_base[:1] + \ command = command_base[:1] + ['-M', '-N'] + command_base[1:]
['-M', '-N'] + \
command_base[1:]
try: try:
Trace(': %s', ' '.join(command)) Trace(': %s', ' '.join(command))
p = subprocess.Popen(command) p = subprocess.Popen(command)
except Exception as e: except Exception as e:
_ssh_master = False _ssh_master = False
print('\nwarn: cannot enable ssh control master for %s:%s\n%s' print('\nwarn: cannot enable ssh control master for %s:%s\n%s'
% (host,port, str(e)), file=sys.stderr) % (host, port, str(e)), file=sys.stderr)
return False return False
time.sleep(1) time.sleep(1)
@ -481,6 +486,7 @@ def _open_ssh(host, port=None):
finally: finally:
_master_keys_lock.release() _master_keys_lock.release()
def close_ssh(): def close_ssh():
global _master_keys_lock global _master_keys_lock
@ -505,15 +511,18 @@ def close_ssh():
# We're done with the lock, so we can delete it. # We're done with the lock, so we can delete it.
_master_keys_lock = None _master_keys_lock = None
URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):') URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):')
URI_ALL = re.compile(r'^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/') URI_ALL = re.compile(r'^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/')
def GetSchemeFromUrl(url): def GetSchemeFromUrl(url):
m = URI_ALL.match(url) m = URI_ALL.match(url)
if m: if m:
return m.group(1) return m.group(1)
return None return None
@contextlib.contextmanager @contextlib.contextmanager
def GetUrlCookieFile(url, quiet): def GetUrlCookieFile(url, quiet):
if url.startswith('persistent-'): if url.startswith('persistent-'):
@ -554,6 +563,7 @@ def GetUrlCookieFile(url, quiet):
cookiefile = os.path.expanduser(cookiefile) cookiefile = os.path.expanduser(cookiefile)
yield cookiefile, None yield cookiefile, None
def _preconnect(url): def _preconnect(url):
m = URI_ALL.match(url) m = URI_ALL.match(url)
if m: if m:
@ -574,9 +584,11 @@ def _preconnect(url):
return False return False
class Remote(object): class Remote(object):
"""Configuration options related to a remote. """Configuration options related to a remote.
""" """
def __init__(self, config, name): def __init__(self, config, name):
self._config = config self._config = config
self.name = name self.name = name
@ -585,7 +597,7 @@ class Remote(object):
self.review = self._Get('review') self.review = self._Get('review')
self.projectname = self._Get('projectname') self.projectname = self._Get('projectname')
self.fetch = list(map(RefSpec.FromString, self.fetch = list(map(RefSpec.FromString,
self._Get('fetch', all_keys=True))) self._Get('fetch', all_keys=True)))
self._review_url = None self._review_url = None
def _InsteadOf(self): def _InsteadOf(self):
@ -599,8 +611,8 @@ class Remote(object):
insteadOfList = globCfg.GetString(key, all_keys=True) insteadOfList = globCfg.GetString(key, all_keys=True)
for insteadOf in insteadOfList: for insteadOf in insteadOfList:
if self.url.startswith(insteadOf) \ if (self.url.startswith(insteadOf)
and len(insteadOf) > len(longest): and len(insteadOf) > len(longest)):
longest = insteadOf longest = insteadOf
longestUrl = url longestUrl = url
@ -731,12 +743,13 @@ class Remote(object):
def _Get(self, key, all_keys=False): def _Get(self, key, all_keys=False):
key = 'remote.%s.%s' % (self.name, key) key = 'remote.%s.%s' % (self.name, key)
return self._config.GetString(key, all_keys = all_keys) return self._config.GetString(key, all_keys=all_keys)
class Branch(object): class Branch(object):
"""Configuration options related to a single branch. """Configuration options related to a single branch.
""" """
def __init__(self, config, name): def __init__(self, config, name):
self._config = config self._config = config
self.name = name self.name = name
@ -780,4 +793,4 @@ class Branch(object):
def _Get(self, key, all_keys=False): def _Get(self, key, all_keys=False):
key = 'branch.%s.%s' % (self.name, key) key = 'branch.%s.%s' % (self.name, key)
return self._config.GetString(key, all_keys = all_keys) return self._config.GetString(key, all_keys=all_keys)

View File

@ -18,12 +18,12 @@ import os
from repo_trace import Trace from repo_trace import Trace
import platform_utils import platform_utils
HEAD = 'HEAD' HEAD = 'HEAD'
R_CHANGES = 'refs/changes/' R_CHANGES = 'refs/changes/'
R_HEADS = 'refs/heads/' R_HEADS = 'refs/heads/'
R_TAGS = 'refs/tags/' R_TAGS = 'refs/tags/'
R_PUB = 'refs/published/' R_PUB = 'refs/published/'
R_M = 'refs/remotes/m/' R_M = 'refs/remotes/m/'
class GitRefs(object): class GitRefs(object):

View File

@ -29,12 +29,15 @@ from error import ManifestParseError
NUM_BATCH_RETRIEVE_REVISIONID = 32 NUM_BATCH_RETRIEVE_REVISIONID = 32
def get_gitc_manifest_dir(): def get_gitc_manifest_dir():
return wrapper.Wrapper().get_gitc_manifest_dir() return wrapper.Wrapper().get_gitc_manifest_dir()
def parse_clientdir(gitc_fs_path): def parse_clientdir(gitc_fs_path):
return wrapper.Wrapper().gitc_parse_clientdir(gitc_fs_path) return wrapper.Wrapper().gitc_parse_clientdir(gitc_fs_path)
def _set_project_revisions(projects): def _set_project_revisions(projects):
"""Sets the revisionExpr for a list of projects. """Sets the revisionExpr for a list of projects.
@ -52,7 +55,7 @@ def _set_project_revisions(projects):
project.remote.url, project.remote.url,
project.revisionExpr], project.revisionExpr],
capture_stdout=True, cwd='/tmp')) capture_stdout=True, cwd='/tmp'))
for project in projects if not git_config.IsId(project.revisionExpr)] for project in projects if not git_config.IsId(project.revisionExpr)]
for proj, gitcmd in project_gitcmds: for proj, gitcmd in project_gitcmds:
if gitcmd.Wait(): if gitcmd.Wait():
print('FATAL: Failed to retrieve revisionExpr for %s' % proj) print('FATAL: Failed to retrieve revisionExpr for %s' % proj)
@ -63,6 +66,7 @@ def _set_project_revisions(projects):
(proj.remote.url, proj.revisionExpr)) (proj.remote.url, proj.revisionExpr))
proj.revisionExpr = revisionExpr proj.revisionExpr = revisionExpr
def _manifest_groups(manifest): def _manifest_groups(manifest):
"""Returns the manifest group string that should be synced """Returns the manifest group string that should be synced
@ -77,6 +81,7 @@ def _manifest_groups(manifest):
groups = 'default,platform-' + platform.system().lower() groups = 'default,platform-' + platform.system().lower()
return groups return groups
def generate_gitc_manifest(gitc_manifest, manifest, paths=None): def generate_gitc_manifest(gitc_manifest, manifest, paths=None):
"""Generate a manifest for shafsd to use for this GITC client. """Generate a manifest for shafsd to use for this GITC client.
@ -104,11 +109,11 @@ def generate_gitc_manifest(gitc_manifest, manifest, paths=None):
if not proj.upstream and not git_config.IsId(proj.revisionExpr): if not proj.upstream and not git_config.IsId(proj.revisionExpr):
proj.upstream = proj.revisionExpr proj.upstream = proj.revisionExpr
if not path in gitc_manifest.paths: if path not in gitc_manifest.paths:
# Any new projects need their first revision, even if we weren't asked # Any new projects need their first revision, even if we weren't asked
# for them. # for them.
projects.append(proj) projects.append(proj)
elif not path in paths: elif path not in paths:
# And copy revisions from the previous manifest if we're not updating # And copy revisions from the previous manifest if we're not updating
# them now. # them now.
gitc_proj = gitc_manifest.paths[path] gitc_proj = gitc_manifest.paths[path]
@ -121,7 +126,7 @@ def generate_gitc_manifest(gitc_manifest, manifest, paths=None):
index = 0 index = 0
while index < len(projects): while index < len(projects):
_set_project_revisions( _set_project_revisions(
projects[index:(index+NUM_BATCH_RETRIEVE_REVISIONID)]) projects[index:(index + NUM_BATCH_RETRIEVE_REVISIONID)])
index += NUM_BATCH_RETRIEVE_REVISIONID index += NUM_BATCH_RETRIEVE_REVISIONID
if gitc_manifest is not None: if gitc_manifest is not None:
@ -140,6 +145,7 @@ def generate_gitc_manifest(gitc_manifest, manifest, paths=None):
# Save the manifest. # Save the manifest.
save_manifest(manifest) save_manifest(manifest)
def save_manifest(manifest, client_dir=None): def save_manifest(manifest, client_dir=None):
"""Save the manifest file in the client_dir. """Save the manifest file in the client_dir.

71
main.py
View File

@ -47,7 +47,7 @@ except ImportError:
from color import SetDefaultColoring from color import SetDefaultColoring
import event_log import event_log
from repo_trace import SetTrace from repo_trace import SetTrace
from git_command import git, GitCommand, user_agent from git_command import user_agent
from git_config import init_ssh, close_ssh from git_config import init_ssh, close_ssh
from command import InteractiveCommand from command import InteractiveCommand
from command import MirrorSafeCommand from command import MirrorSafeCommand
@ -101,6 +101,7 @@ global_options.add_option('--event-log',
dest='event_log', action='store', dest='event_log', action='store',
help='filename of event log to append timeline to') help='filename of event log to append timeline to')
class _Repo(object): class _Repo(object):
def __init__(self, repodir): def __init__(self, repodir):
self.repodir = repodir self.repodir = repodir
@ -188,7 +189,7 @@ class _Repo(object):
copts = cmd.ReadEnvironmentOptions(copts) copts = cmd.ReadEnvironmentOptions(copts)
except NoManifestException as e: except NoManifestException as e:
print('error: in `%s`: %s' % (' '.join([name] + argv), str(e)), print('error: in `%s`: %s' % (' '.join([name] + argv), str(e)),
file=sys.stderr) file=sys.stderr)
print('error: manifest missing or unreadable -- please run init', print('error: manifest missing or unreadable -- please run init',
file=sys.stderr) file=sys.stderr)
return 1 return 1
@ -211,9 +212,9 @@ class _Repo(object):
cmd.ValidateOptions(copts, cargs) cmd.ValidateOptions(copts, cargs)
result = cmd.Execute(copts, cargs) result = cmd.Execute(copts, cargs)
except (DownloadError, ManifestInvalidRevisionError, except (DownloadError, ManifestInvalidRevisionError,
NoManifestException) as e: NoManifestException) as e:
print('error: in `%s`: %s' % (' '.join([name] + argv), str(e)), print('error: in `%s`: %s' % (' '.join([name] + argv), str(e)),
file=sys.stderr) file=sys.stderr)
if isinstance(e, NoManifestException): if isinstance(e, NoManifestException):
print('error: manifest missing or unreadable -- please run init', print('error: manifest missing or unreadable -- please run init',
file=sys.stderr) file=sys.stderr)
@ -255,27 +256,41 @@ class _Repo(object):
return result return result
def _CheckWrapperVersion(ver, repo_path): def _CheckWrapperVersion(ver_str, repo_path):
"""Verify the repo launcher is new enough for this checkout.
Args:
ver_str: The version string passed from the repo launcher when it ran us.
repo_path: The path to the repo launcher that loaded us.
"""
# Refuse to work with really old wrapper versions. We don't test these,
# so might as well require a somewhat recent sane version.
# v1.15 of the repo launcher was released in ~Mar 2012.
MIN_REPO_VERSION = (1, 15)
min_str = '.'.join(str(x) for x in MIN_REPO_VERSION)
if not repo_path: if not repo_path:
repo_path = '~/bin/repo' repo_path = '~/bin/repo'
if not ver: if not ver_str:
print('no --wrapper-version argument', file=sys.stderr) print('no --wrapper-version argument', file=sys.stderr)
sys.exit(1) sys.exit(1)
# Pull out the version of the repo launcher we know about to compare.
exp = Wrapper().VERSION exp = Wrapper().VERSION
ver = tuple(map(int, ver.split('.'))) ver = tuple(map(int, ver_str.split('.')))
if len(ver) == 1:
ver = (0, ver[0])
exp_str = '.'.join(map(str, exp)) exp_str = '.'.join(map(str, exp))
if exp[0] > ver[0] or ver < (0, 4): if ver < MIN_REPO_VERSION:
print(""" print("""
!!! A new repo command (%5s) is available. !!! repo: error:
!!! You must upgrade before you can continue: !!! !!! Your version of repo %s is too old.
!!! We need at least version %s.
!!! A new repo command (%s) is available.
!!! You must upgrade before you can continue:
cp %s %s cp %s %s
""" % (exp_str, WrapperPath(), repo_path), file=sys.stderr) """ % (ver_str, min_str, exp_str, WrapperPath(), repo_path), file=sys.stderr)
sys.exit(1) sys.exit(1)
if exp > ver: if exp > ver:
@ -286,11 +301,13 @@ def _CheckWrapperVersion(ver, repo_path):
cp %s %s cp %s %s
""" % (exp_str, WrapperPath(), repo_path), file=sys.stderr) """ % (exp_str, WrapperPath(), repo_path), file=sys.stderr)
def _CheckRepoDir(repo_dir): def _CheckRepoDir(repo_dir):
if not repo_dir: if not repo_dir:
print('no --repo-dir argument', file=sys.stderr) print('no --repo-dir argument', file=sys.stderr)
sys.exit(1) sys.exit(1)
def _PruneOptions(argv, opt): def _PruneOptions(argv, opt):
i = 0 i = 0
while i < len(argv): while i < len(argv):
@ -306,6 +323,7 @@ def _PruneOptions(argv, opt):
continue continue
i += 1 i += 1
class _UserAgentHandler(urllib.request.BaseHandler): class _UserAgentHandler(urllib.request.BaseHandler):
def http_request(self, req): def http_request(self, req):
req.add_header('User-Agent', user_agent.repo) req.add_header('User-Agent', user_agent.repo)
@ -315,6 +333,7 @@ class _UserAgentHandler(urllib.request.BaseHandler):
req.add_header('User-Agent', user_agent.repo) req.add_header('User-Agent', user_agent.repo)
return req return req
def _AddPasswordFromUserInput(handler, msg, req): def _AddPasswordFromUserInput(handler, msg, req):
# If repo could not find auth info from netrc, try to get it from user input # If repo could not find auth info from netrc, try to get it from user input
url = req.get_full_url() url = req.get_full_url()
@ -328,22 +347,24 @@ def _AddPasswordFromUserInput(handler, msg, req):
return return
handler.passwd.add_password(None, url, user, password) handler.passwd.add_password(None, url, user, password)
class _BasicAuthHandler(urllib.request.HTTPBasicAuthHandler): class _BasicAuthHandler(urllib.request.HTTPBasicAuthHandler):
def http_error_401(self, req, fp, code, msg, headers): def http_error_401(self, req, fp, code, msg, headers):
_AddPasswordFromUserInput(self, msg, req) _AddPasswordFromUserInput(self, msg, req)
return urllib.request.HTTPBasicAuthHandler.http_error_401( return urllib.request.HTTPBasicAuthHandler.http_error_401(
self, req, fp, code, msg, headers) self, req, fp, code, msg, headers)
def http_error_auth_reqed(self, authreq, host, req, headers): def http_error_auth_reqed(self, authreq, host, req, headers):
try: try:
old_add_header = req.add_header old_add_header = req.add_header
def _add_header(name, val): def _add_header(name, val):
val = val.replace('\n', '') val = val.replace('\n', '')
old_add_header(name, val) old_add_header(name, val)
req.add_header = _add_header req.add_header = _add_header
return urllib.request.AbstractBasicAuthHandler.http_error_auth_reqed( return urllib.request.AbstractBasicAuthHandler.http_error_auth_reqed(
self, authreq, host, req, headers) self, authreq, host, req, headers)
except: except Exception:
reset = getattr(self, 'reset_retry_count', None) reset = getattr(self, 'reset_retry_count', None)
if reset is not None: if reset is not None:
reset() reset()
@ -351,22 +372,24 @@ class _BasicAuthHandler(urllib.request.HTTPBasicAuthHandler):
self.retried = 0 self.retried = 0
raise raise
class _DigestAuthHandler(urllib.request.HTTPDigestAuthHandler): class _DigestAuthHandler(urllib.request.HTTPDigestAuthHandler):
def http_error_401(self, req, fp, code, msg, headers): def http_error_401(self, req, fp, code, msg, headers):
_AddPasswordFromUserInput(self, msg, req) _AddPasswordFromUserInput(self, msg, req)
return urllib.request.HTTPDigestAuthHandler.http_error_401( return urllib.request.HTTPDigestAuthHandler.http_error_401(
self, req, fp, code, msg, headers) self, req, fp, code, msg, headers)
def http_error_auth_reqed(self, auth_header, host, req, headers): def http_error_auth_reqed(self, auth_header, host, req, headers):
try: try:
old_add_header = req.add_header old_add_header = req.add_header
def _add_header(name, val): def _add_header(name, val):
val = val.replace('\n', '') val = val.replace('\n', '')
old_add_header(name, val) old_add_header(name, val)
req.add_header = _add_header req.add_header = _add_header
return urllib.request.AbstractDigestAuthHandler.http_error_auth_reqed( return urllib.request.AbstractDigestAuthHandler.http_error_auth_reqed(
self, auth_header, host, req, headers) self, auth_header, host, req, headers)
except: except Exception:
reset = getattr(self, 'reset_retry_count', None) reset = getattr(self, 'reset_retry_count', None)
if reset is not None: if reset is not None:
reset() reset()
@ -374,6 +397,7 @@ class _DigestAuthHandler(urllib.request.HTTPDigestAuthHandler):
self.retried = 0 self.retried = 0
raise raise
class _KerberosAuthHandler(urllib.request.BaseHandler): class _KerberosAuthHandler(urllib.request.BaseHandler):
def __init__(self): def __init__(self):
self.retried = 0 self.retried = 0
@ -392,7 +416,7 @@ class _KerberosAuthHandler(urllib.request.BaseHandler):
if self.retried > 3: if self.retried > 3:
raise urllib.request.HTTPError(req.get_full_url(), 401, raise urllib.request.HTTPError(req.get_full_url(), 401,
"Negotiate auth failed", headers, None) "Negotiate auth failed", headers, None)
else: else:
self.retried += 1 self.retried += 1
@ -408,7 +432,7 @@ class _KerberosAuthHandler(urllib.request.BaseHandler):
return response return response
except kerberos.GSSError: except kerberos.GSSError:
return None return None
except: except Exception:
self.reset_retry_count() self.reset_retry_count()
raise raise
finally: finally:
@ -454,6 +478,7 @@ class _KerberosAuthHandler(urllib.request.BaseHandler):
kerberos.authGSSClientClean(self.context) kerberos.authGSSClientClean(self.context)
self.context = None self.context = None
def init_http(): def init_http():
handlers = [_UserAgentHandler()] handlers = [_UserAgentHandler()]
@ -462,7 +487,7 @@ def init_http():
n = netrc.netrc() n = netrc.netrc()
for host in n.hosts: for host in n.hosts:
p = n.hosts[host] p = n.hosts[host]
mgr.add_password(p[1], 'http://%s/' % host, p[0], p[2]) mgr.add_password(p[1], 'http://%s/' % host, p[0], p[2])
mgr.add_password(p[1], 'https://%s/' % host, p[0], p[2]) mgr.add_password(p[1], 'https://%s/' % host, p[0], p[2])
except netrc.NetrcParseError: except netrc.NetrcParseError:
pass pass
@ -481,6 +506,7 @@ def init_http():
handlers.append(urllib.request.HTTPSHandler(debuglevel=1)) handlers.append(urllib.request.HTTPSHandler(debuglevel=1))
urllib.request.install_opener(urllib.request.build_opener(*handlers)) urllib.request.install_opener(urllib.request.build_opener(*handlers))
def _Main(argv): def _Main(argv):
result = 0 result = 0
@ -537,5 +563,6 @@ def _Main(argv):
TerminatePager() TerminatePager()
sys.exit(result) sys.exit(result)
if __name__ == '__main__': if __name__ == '__main__':
_Main(sys.argv[1:]) _Main(sys.argv[1:])

View File

@ -56,6 +56,7 @@ urllib.parse.uses_netloc.extend([
'sso', 'sso',
'rpc']) 'rpc'])
class _Default(object): class _Default(object):
"""Project defaults within the manifest.""" """Project defaults within the manifest."""
@ -74,6 +75,7 @@ class _Default(object):
def __ne__(self, other): def __ne__(self, other):
return self.__dict__ != other.__dict__ return self.__dict__ != other.__dict__
class _XmlRemote(object): class _XmlRemote(object):
def __init__(self, def __init__(self,
name, name,
@ -127,6 +129,7 @@ class _XmlRemote(object):
orig_name=self.name, orig_name=self.name,
fetchUrl=self.fetchUrl) fetchUrl=self.fetchUrl)
class XmlManifest(object): class XmlManifest(object):
"""manages the repo configuration file""" """manages the repo configuration file"""
@ -140,12 +143,12 @@ class XmlManifest(object):
self._load_local_manifests = True self._load_local_manifests = True
self.repoProject = MetaProject(self, 'repo', self.repoProject = MetaProject(self, 'repo',
gitdir = os.path.join(repodir, 'repo/.git'), gitdir=os.path.join(repodir, 'repo/.git'),
worktree = os.path.join(repodir, 'repo')) worktree=os.path.join(repodir, 'repo'))
self.manifestProject = MetaProject(self, 'manifests', self.manifestProject = MetaProject(self, 'manifests',
gitdir = os.path.join(repodir, 'manifests.git'), gitdir=os.path.join(repodir, 'manifests.git'),
worktree = os.path.join(repodir, 'manifests')) worktree=os.path.join(repodir, 'manifests'))
self._Unload() self._Unload()
@ -224,7 +227,7 @@ class XmlManifest(object):
if self.notice: if self.notice:
notice_element = root.appendChild(doc.createElement('notice')) notice_element = root.appendChild(doc.createElement('notice'))
notice_lines = self.notice.splitlines() notice_lines = self.notice.splitlines()
indented_notice = ('\n'.join(" "*4 + line for line in notice_lines))[4:] indented_notice = ('\n'.join(" " * 4 + line for line in notice_lines))[4:]
notice_element.appendChild(doc.createTextNode(indented_notice)) notice_element.appendChild(doc.createTextNode(indented_notice))
d = self.default d = self.default
@ -462,12 +465,12 @@ class XmlManifest(object):
self.localManifestWarning = True self.localManifestWarning = True
print('warning: %s is deprecated; put local manifests ' print('warning: %s is deprecated; put local manifests '
'in `%s` instead' % (LOCAL_MANIFEST_NAME, 'in `%s` instead' % (LOCAL_MANIFEST_NAME,
os.path.join(self.repodir, LOCAL_MANIFESTS_DIR_NAME)), os.path.join(self.repodir, LOCAL_MANIFESTS_DIR_NAME)),
file=sys.stderr) file=sys.stderr)
nodes.append(self._ParseManifestXml(local, self.repodir)) nodes.append(self._ParseManifestXml(local, self.repodir))
local_dir = os.path.abspath(os.path.join(self.repodir, local_dir = os.path.abspath(os.path.join(self.repodir,
LOCAL_MANIFESTS_DIR_NAME)) LOCAL_MANIFESTS_DIR_NAME))
try: try:
for local_file in sorted(platform_utils.listdir(local_dir)): for local_file in sorted(platform_utils.listdir(local_dir)):
if local_file.endswith('.xml'): if local_file.endswith('.xml'):
@ -512,7 +515,7 @@ class XmlManifest(object):
fp = os.path.join(include_root, name) fp = os.path.join(include_root, name)
if not os.path.isfile(fp): if not os.path.isfile(fp):
raise ManifestParseError("include %s doesn't exist or isn't a file" raise ManifestParseError("include %s doesn't exist or isn't a file"
% (name,)) % (name,))
try: try:
nodes.extend(self._ParseManifestXml(fp, include_root)) nodes.extend(self._ParseManifestXml(fp, include_root))
# should isolate this to the exact exception, but that's # should isolate this to the exact exception, but that's
@ -655,7 +658,6 @@ class XmlManifest(object):
if self._repo_hooks_project and (self._repo_hooks_project.name == name): if self._repo_hooks_project and (self._repo_hooks_project.name == name):
self._repo_hooks_project = None self._repo_hooks_project = None
def _AddMetaProjectMirror(self, m): def _AddMetaProjectMirror(self, m):
name = None name = None
m_url = m.GetRemote(m.remote.name).url m_url = m.GetRemote(m.remote.name).url
@ -682,15 +684,15 @@ class XmlManifest(object):
if name not in self._projects: if name not in self._projects:
m.PreSync() m.PreSync()
gitdir = os.path.join(self.topdir, '%s.git' % name) gitdir = os.path.join(self.topdir, '%s.git' % name)
project = Project(manifest = self, project = Project(manifest=self,
name = name, name=name,
remote = remote.ToRemoteSpec(name), remote=remote.ToRemoteSpec(name),
gitdir = gitdir, gitdir=gitdir,
objdir = gitdir, objdir=gitdir,
worktree = None, worktree=None,
relpath = name or None, relpath=name or None,
revisionExpr = m.revisionExpr, revisionExpr=m.revisionExpr,
revisionId = None) revisionId=None)
self._projects[project.name] = [project] self._projects[project.name] = [project]
self._paths[project.relpath] = project self._paths[project.relpath] = project
@ -798,7 +800,7 @@ class XmlManifest(object):
def _UnjoinName(self, parent_name, name): def _UnjoinName(self, parent_name, name):
return os.path.relpath(name, parent_name) return os.path.relpath(name, parent_name)
def _ParseProject(self, node, parent = None, **extra_proj_attrs): def _ParseProject(self, node, parent=None, **extra_proj_attrs):
""" """
reads a <project> element from the manifest file reads a <project> element from the manifest file
""" """
@ -811,21 +813,21 @@ class XmlManifest(object):
remote = self._default.remote remote = self._default.remote
if remote is None: if remote is None:
raise ManifestParseError("no remote for project %s within %s" % raise ManifestParseError("no remote for project %s within %s" %
(name, self.manifestFile)) (name, self.manifestFile))
revisionExpr = node.getAttribute('revision') or remote.revision revisionExpr = node.getAttribute('revision') or remote.revision
if not revisionExpr: if not revisionExpr:
revisionExpr = self._default.revisionExpr revisionExpr = self._default.revisionExpr
if not revisionExpr: if not revisionExpr:
raise ManifestParseError("no revision for project %s within %s" % raise ManifestParseError("no revision for project %s within %s" %
(name, self.manifestFile)) (name, self.manifestFile))
path = node.getAttribute('path') path = node.getAttribute('path')
if not path: if not path:
path = name path = name
if path.startswith('/'): if path.startswith('/'):
raise ManifestParseError("project %s path cannot be absolute in %s" % raise ManifestParseError("project %s path cannot be absolute in %s" %
(name, self.manifestFile)) (name, self.manifestFile))
rebase = node.getAttribute('rebase') rebase = node.getAttribute('rebase')
if not rebase: if not rebase:
@ -855,7 +857,7 @@ class XmlManifest(object):
if clone_depth: if clone_depth:
try: try:
clone_depth = int(clone_depth) clone_depth = int(clone_depth)
if clone_depth <= 0: if clone_depth <= 0:
raise ValueError() raise ValueError()
except ValueError: except ValueError:
raise ManifestParseError('invalid clone-depth %s in %s' % raise ManifestParseError('invalid clone-depth %s in %s' %
@ -883,24 +885,24 @@ class XmlManifest(object):
if node.getAttribute('force-path').lower() in ("yes", "true", "1"): if node.getAttribute('force-path').lower() in ("yes", "true", "1"):
gitdir = os.path.join(self.topdir, '%s.git' % path) gitdir = os.path.join(self.topdir, '%s.git' % path)
project = Project(manifest = self, project = Project(manifest=self,
name = name, name=name,
remote = remote.ToRemoteSpec(name), remote=remote.ToRemoteSpec(name),
gitdir = gitdir, gitdir=gitdir,
objdir = objdir, objdir=objdir,
worktree = worktree, worktree=worktree,
relpath = relpath, relpath=relpath,
revisionExpr = revisionExpr, revisionExpr=revisionExpr,
revisionId = None, revisionId=None,
rebase = rebase, rebase=rebase,
groups = groups, groups=groups,
sync_c = sync_c, sync_c=sync_c,
sync_s = sync_s, sync_s=sync_s,
sync_tags = sync_tags, sync_tags=sync_tags,
clone_depth = clone_depth, clone_depth=clone_depth,
upstream = upstream, upstream=upstream,
parent = parent, parent=parent,
dest_branch = dest_branch, dest_branch=dest_branch,
**extra_proj_attrs) **extra_proj_attrs)
for n in node.childNodes: for n in node.childNodes:
@ -911,7 +913,7 @@ class XmlManifest(object):
if n.nodeName == 'annotation': if n.nodeName == 'annotation':
self._ParseAnnotation(project, n) self._ParseAnnotation(project, n)
if n.nodeName == 'project': if n.nodeName == 'project':
project.subprojects.append(self._ParseProject(n, parent = project)) project.subprojects.append(self._ParseProject(n, parent=project))
return project return project
@ -985,11 +987,13 @@ class XmlManifest(object):
# Assume paths might be used on case-insensitive filesystems. # Assume paths might be used on case-insensitive filesystems.
path = path.lower() path = path.lower()
# We don't really need to reject '.' here, but there shouldn't really be a # Some people use src="." to create stable links to projects. Lets allow
# need to ever use it, so no need to accept it either. # that but reject all other uses of "." to keep things simple.
for part in set(path.split(os.path.sep)): parts = path.split(os.path.sep)
if part in {'.', '..', '.git'} or part.startswith('.repo'): if parts != ['.']:
return 'bad component: %s' % (part,) for part in set(parts):
if part in {'.', '..', '.git'} or part.startswith('.repo'):
return 'bad component: %s' % (part,)
if not symlink and path.endswith(os.path.sep): if not symlink and path.endswith(os.path.sep):
return 'dirs not allowed' return 'dirs not allowed'
@ -1052,7 +1056,7 @@ class XmlManifest(object):
keep = "true" keep = "true"
if keep != "true" and keep != "false": if keep != "true" and keep != "false":
raise ManifestParseError('optional "keep" attribute must be ' raise ManifestParseError('optional "keep" attribute must be '
'"true" or "false"') '"true" or "false"')
project.AddAnnotation(name, value, keep) project.AddAnnotation(name, value, keep)
def _get_remote(self, node): def _get_remote(self, node):
@ -1063,7 +1067,7 @@ class XmlManifest(object):
v = self._remotes.get(name) v = self._remotes.get(name)
if not v: if not v:
raise ManifestParseError("remote %s not defined in %s" % raise ManifestParseError("remote %s not defined in %s" %
(name, self.manifestFile)) (name, self.manifestFile))
return v return v
def _reqatt(self, node, attname): def _reqatt(self, node, attname):
@ -1073,7 +1077,7 @@ class XmlManifest(object):
v = node.getAttribute(attname) v = node.getAttribute(attname)
if not v: if not v:
raise ManifestParseError("no %s in <%s> within %s" % raise ManifestParseError("no %s in <%s> within %s" %
(attname, node.nodeName, self.manifestFile)) (attname, node.nodeName, self.manifestFile))
return v return v
def projectsDiff(self, manifest): def projectsDiff(self, manifest):
@ -1091,7 +1095,7 @@ class XmlManifest(object):
diff = {'added': [], 'removed': [], 'changed': [], 'unreachable': []} diff = {'added': [], 'removed': [], 'changed': [], 'unreachable': []}
for proj in fromKeys: for proj in fromKeys:
if not proj in toKeys: if proj not in toKeys:
diff['removed'].append(fromProjects[proj]) diff['removed'].append(fromProjects[proj])
else: else:
fromProj = fromProjects[proj] fromProj = fromProjects[proj]
@ -1123,7 +1127,7 @@ class GitcManifest(XmlManifest):
gitc_client_name) gitc_client_name)
self.manifestFile = os.path.join(self.gitc_client_dir, '.manifest') self.manifestFile = os.path.join(self.gitc_client_dir, '.manifest')
def _ParseProject(self, node, parent = None): def _ParseProject(self, node, parent=None):
"""Override _ParseProject and add support for GITC specific attributes.""" """Override _ParseProject and add support for GITC specific attributes."""
return super(GitcManifest, self)._ParseProject( return super(GitcManifest, self)._ParseProject(
node, parent=parent, old_revision=node.getAttribute('old-revision')) node, parent=parent, old_revision=node.getAttribute('old-revision'))
@ -1132,4 +1136,3 @@ class GitcManifest(XmlManifest):
"""Output GITC Specific Project attributes""" """Output GITC Specific Project attributes"""
if p.old_revision: if p.old_revision:
e.setAttribute('old-revision', str(p.old_revision)) e.setAttribute('old-revision', str(p.old_revision))

View File

@ -27,6 +27,7 @@ pager_process = None
old_stdout = None old_stdout = None
old_stderr = None old_stderr = None
def RunPager(globalConfig): def RunPager(globalConfig):
if not os.isatty(0) or not os.isatty(1): if not os.isatty(0) or not os.isatty(1):
return return
@ -35,23 +36,25 @@ def RunPager(globalConfig):
return return
if platform_utils.isWindows(): if platform_utils.isWindows():
_PipePager(pager); _PipePager(pager)
else: else:
_ForkPager(pager) _ForkPager(pager)
def TerminatePager(): def TerminatePager():
global pager_process, old_stdout, old_stderr global pager_process, old_stdout, old_stderr
if pager_process: if pager_process:
sys.stdout.flush() sys.stdout.flush()
sys.stderr.flush() sys.stderr.flush()
pager_process.stdin.close() pager_process.stdin.close()
pager_process.wait(); pager_process.wait()
pager_process = None pager_process = None
# Restore initial stdout/err in case there is more output in this process # Restore initial stdout/err in case there is more output in this process
# after shutting down the pager process # after shutting down the pager process
sys.stdout = old_stdout sys.stdout = old_stdout
sys.stderr = old_stderr sys.stderr = old_stderr
def _PipePager(pager): def _PipePager(pager):
global pager_process, old_stdout, old_stderr global pager_process, old_stdout, old_stderr
assert pager_process is None, "Only one active pager process at a time" assert pager_process is None, "Only one active pager process at a time"
@ -62,6 +65,7 @@ def _PipePager(pager):
sys.stdout = pager_process.stdin sys.stdout = pager_process.stdin
sys.stderr = pager_process.stdin sys.stderr = pager_process.stdin
def _ForkPager(pager): def _ForkPager(pager):
global active global active
# This process turns into the pager; a child it forks will # This process turns into the pager; a child it forks will
@ -88,6 +92,7 @@ def _ForkPager(pager):
print("fatal: cannot start pager '%s'" % pager, file=sys.stderr) print("fatal: cannot start pager '%s'" % pager, file=sys.stderr)
sys.exit(255) sys.exit(255)
def _SelectPager(globalConfig): def _SelectPager(globalConfig):
try: try:
return os.environ['GIT_PAGER'] return os.environ['GIT_PAGER']
@ -105,6 +110,7 @@ def _SelectPager(globalConfig):
return 'less' return 'less'
def _BecomePager(pager): def _BecomePager(pager):
# Delaying execution of the pager until we have output # Delaying execution of the pager until we have output
# ready works around a long-standing bug in popularly # ready works around a long-standing bug in popularly

View File

@ -92,6 +92,7 @@ class _FileDescriptorStreamsNonBlocking(FileDescriptorStreams):
""" """
class Stream(object): class Stream(object):
""" Encapsulates a file descriptor """ """ Encapsulates a file descriptor """
def __init__(self, fd, dest, std_name): def __init__(self, fd, dest, std_name):
self.fd = fd self.fd = fd
self.dest = dest self.dest = dest
@ -125,6 +126,7 @@ class _FileDescriptorStreamsThreads(FileDescriptorStreams):
non blocking I/O. This implementation requires creating threads issuing non blocking I/O. This implementation requires creating threads issuing
blocking read operations on file descriptors. blocking read operations on file descriptors.
""" """
def __init__(self): def __init__(self):
super(_FileDescriptorStreamsThreads, self).__init__() super(_FileDescriptorStreamsThreads, self).__init__()
# The queue is shared accross all threads so we can simulate the # The queue is shared accross all threads so we can simulate the
@ -144,12 +146,14 @@ class _FileDescriptorStreamsThreads(FileDescriptorStreams):
class QueueItem(object): class QueueItem(object):
""" Item put in the shared queue """ """ Item put in the shared queue """
def __init__(self, stream, data): def __init__(self, stream, data):
self.stream = stream self.stream = stream
self.data = data self.data = data
class Stream(object): class Stream(object):
""" Encapsulates a file descriptor """ """ Encapsulates a file descriptor """
def __init__(self, fd, dest, std_name, queue): def __init__(self, fd, dest, std_name, queue):
self.fd = fd self.fd = fd
self.dest = dest self.dest = dest

View File

@ -26,6 +26,7 @@ _NOT_TTY = not os.isatty(2)
# column 0. # column 0.
CSI_ERASE_LINE = '\x1b[2K' CSI_ERASE_LINE = '\x1b[2K'
class Progress(object): class Progress(object):
def __init__(self, title, total=0, units='', print_newline=False, def __init__(self, title, total=0, units='', print_newline=False,
always_print_percentage=False): always_print_percentage=False):
@ -53,9 +54,9 @@ class Progress(object):
if self._total <= 0: if self._total <= 0:
sys.stderr.write('%s\r%s: %d,' % ( sys.stderr.write('%s\r%s: %d,' % (
CSI_ERASE_LINE, CSI_ERASE_LINE,
self._title, self._title,
self._done)) self._done))
sys.stderr.flush() sys.stderr.flush()
else: else:
p = (100 * self._done) / self._total p = (100 * self._done) / self._total
@ -63,13 +64,13 @@ class Progress(object):
if self._lastp != p or self._always_print_percentage: if self._lastp != p or self._always_print_percentage:
self._lastp = p self._lastp = p
sys.stderr.write('%s\r%s: %3d%% (%d%s/%d%s)%s%s%s' % ( sys.stderr.write('%s\r%s: %3d%% (%d%s/%d%s)%s%s%s' % (
CSI_ERASE_LINE, CSI_ERASE_LINE,
self._title, self._title,
p, p,
self._done, self._units, self._done, self._units,
self._total, self._units, self._total, self._units,
' ' if msg else '', msg, ' ' if msg else '', msg,
"\n" if self._print_newline else "")) "\n" if self._print_newline else ""))
sys.stderr.flush() sys.stderr.flush()
def end(self): def end(self):
@ -78,16 +79,16 @@ class Progress(object):
if self._total <= 0: if self._total <= 0:
sys.stderr.write('%s\r%s: %d, done.\n' % ( sys.stderr.write('%s\r%s: %d, done.\n' % (
CSI_ERASE_LINE, CSI_ERASE_LINE,
self._title, self._title,
self._done)) self._done))
sys.stderr.flush() sys.stderr.flush()
else: else:
p = (100 * self._done) / self._total p = (100 * self._done) / self._total
sys.stderr.write('%s\r%s: %3d%% (%d%s/%d%s), done.\n' % ( sys.stderr.write('%s\r%s: %3d%% (%d%s/%d%s), done.\n' % (
CSI_ERASE_LINE, CSI_ERASE_LINE,
self._title, self._title,
p, p,
self._done, self._units, self._done, self._units,
self._total, self._units)) self._total, self._units))
sys.stderr.flush() sys.stderr.flush()

View File

@ -85,6 +85,7 @@ def not_rev(r):
def sq(r): def sq(r):
return "'" + r.replace("'", "'\''") + "'" return "'" + r.replace("'", "'\''") + "'"
_project_hook_list = None _project_hook_list = None
@ -382,7 +383,12 @@ class _LinkFile(object):
Handles wild cards on the src linking all of the files in the source in to Handles wild cards on the src linking all of the files in the source in to
the destination directory. the destination directory.
""" """
src = _SafeExpandPath(self.git_worktree, self.src) # Some people use src="." to create stable links to projects. Lets allow
# that but reject all other uses of "." to keep things simple.
if self.src == '.':
src = self.git_worktree
else:
src = _SafeExpandPath(self.git_worktree, self.src)
if os.path.exists(src): if os.path.exists(src):
# Entity exists so just a simple one to one link operation. # Entity exists so just a simple one to one link operation.
@ -1251,9 +1257,7 @@ class Project(object):
print(line[:-1]) print(line[:-1])
return p.Wait() == 0 return p.Wait() == 0
# Publish / Upload ## # Publish / Upload ##
def WasPublished(self, branch, all_refs=None): def WasPublished(self, branch, all_refs=None):
"""Was the branch published (uploaded) for code review? """Was the branch published (uploaded) for code review?
If so, returns the SHA-1 hash of the last published If so, returns the SHA-1 hash of the last published
@ -1405,9 +1409,7 @@ class Project(object):
R_HEADS + branch.name, R_HEADS + branch.name,
message=msg) message=msg)
# Sync ## # Sync ##
def _ExtractArchive(self, tarpath, path=None): def _ExtractArchive(self, tarpath, path=None):
"""Extract the given tar on its current location """Extract the given tar on its current location
@ -1814,9 +1816,7 @@ class Project(object):
patch_id, patch_id,
self.bare_git.rev_parse('FETCH_HEAD')) self.bare_git.rev_parse('FETCH_HEAD'))
# Branch Management ## # Branch Management ##
def GetHeadPath(self): def GetHeadPath(self):
"""Return the full path to the HEAD ref.""" """Return the full path to the HEAD ref."""
dotgit = os.path.join(self.worktree, '.git') dotgit = os.path.join(self.worktree, '.git')
@ -2014,9 +2014,7 @@ class Project(object):
kept.append(ReviewableBranch(self, branch, base)) kept.append(ReviewableBranch(self, branch, base))
return kept return kept
# Submodule Management ## # Submodule Management ##
def GetRegisteredSubprojects(self): def GetRegisteredSubprojects(self):
result = [] result = []
@ -2068,7 +2066,7 @@ class Project(object):
gitmodules_lines = [] gitmodules_lines = []
fd, temp_gitmodules_path = tempfile.mkstemp() fd, temp_gitmodules_path = tempfile.mkstemp()
try: try:
os.write(fd, p.stdout) os.write(fd, p.stdout.encode('utf-8'))
os.close(fd) os.close(fd)
cmd = ['config', '--file', temp_gitmodules_path, '--list'] cmd = ['config', '--file', temp_gitmodules_path, '--list']
p = GitCommand(None, cmd, capture_stdout=True, capture_stderr=True, p = GitCommand(None, cmd, capture_stdout=True, capture_stderr=True,
@ -2167,7 +2165,6 @@ class Project(object):
result.extend(subproject.GetDerivedSubprojects()) result.extend(subproject.GetDerivedSubprojects())
return result return result
# Direct Git Commands ## # Direct Git Commands ##
def _CheckForImmutableRevision(self): def _CheckForImmutableRevision(self):
try: try:
@ -2336,7 +2333,7 @@ class Project(object):
else: else:
branch = self.revisionExpr branch = self.revisionExpr
if (not self.manifest.IsMirror and is_sha1 and depth if (not self.manifest.IsMirror and is_sha1 and depth
and git_require((1, 8, 3))): and git_require((1, 8, 3))):
# Shallow checkout of a specific commit, fetch from that commit and not # Shallow checkout of a specific commit, fetch from that commit and not
# the heads only as the commit might be deeper in the history. # the heads only as the commit might be deeper in the history.
spec.append(branch) spec.append(branch)
@ -2477,7 +2474,7 @@ class Project(object):
platform_utils.remove(tmpPath) platform_utils.remove(tmpPath)
with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy): with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
if cookiefile: if cookiefile:
cmd += ['--cookie', cookiefile, '--cookie-jar', cookiefile] cmd += ['--cookie', cookiefile]
if proxy: if proxy:
cmd += ['--proxy', proxy] cmd += ['--proxy', proxy]
elif 'http_proxy' in os.environ and 'darwin' == sys.platform: elif 'http_proxy' in os.environ and 'darwin' == sys.platform:
@ -2622,7 +2619,7 @@ class Project(object):
(self.worktree)): (self.worktree)):
platform_utils.rmtree(platform_utils.realpath(self.worktree)) platform_utils.rmtree(platform_utils.realpath(self.worktree))
return self._InitGitDir(mirror_git=mirror_git, force_sync=False) return self._InitGitDir(mirror_git=mirror_git, force_sync=False)
except: except Exception:
raise e raise e
raise e raise e
@ -2755,9 +2752,31 @@ class Project(object):
symlink_dirs += self.working_tree_dirs symlink_dirs += self.working_tree_dirs
to_symlink = symlink_files + symlink_dirs to_symlink = symlink_files + symlink_dirs
for name in set(to_symlink): for name in set(to_symlink):
dst = platform_utils.realpath(os.path.join(destdir, name)) # Try to self-heal a bit in simple cases.
dst_path = os.path.join(destdir, name)
src_path = os.path.join(srcdir, name)
if name in self.working_tree_dirs:
# If the dir is missing under .repo/projects/, create it.
if not os.path.exists(src_path):
os.makedirs(src_path)
elif name in self.working_tree_files:
# If it's a file under the checkout .git/ and the .repo/projects/ has
# nothing, move the file under the .repo/projects/ tree.
if not os.path.exists(src_path) and os.path.isfile(dst_path):
platform_utils.rename(dst_path, src_path)
# If the path exists under the .repo/projects/ and there's no symlink
# under the checkout .git/, recreate the symlink.
if name in self.working_tree_dirs or name in self.working_tree_files:
if os.path.exists(src_path) and not os.path.exists(dst_path):
platform_utils.symlink(
os.path.relpath(src_path, os.path.dirname(dst_path)), dst_path)
dst = platform_utils.realpath(dst_path)
if os.path.lexists(dst): if os.path.lexists(dst):
src = platform_utils.realpath(os.path.join(srcdir, name)) src = platform_utils.realpath(src_path)
# Fail if the links are pointing to the wrong place # Fail if the links are pointing to the wrong place
if src != dst: if src != dst:
_error('%s is different in %s vs %s', name, destdir, srcdir) _error('%s is different in %s vs %s', name, destdir, srcdir)
@ -2845,7 +2864,7 @@ class Project(object):
try: try:
platform_utils.rmtree(dotgit) platform_utils.rmtree(dotgit)
return self._InitWorkTree(force_sync=False, submodules=submodules) return self._InitWorkTree(force_sync=False, submodules=submodules)
except: except Exception:
raise e raise e
raise e raise e
@ -3106,9 +3125,6 @@ class Project(object):
raise TypeError('%s() got an unexpected keyword argument %r' raise TypeError('%s() got an unexpected keyword argument %r'
% (name, k)) % (name, k))
if config is not None: if config is not None:
if not git_require((1, 7, 2)):
raise ValueError('cannot set config on command line for %s()'
% name)
for k, v in config.items(): for k, v in config.items():
cmdv.append('-c') cmdv.append('-c')
cmdv.append('%s=%s' % (k, v)) cmdv.append('%s=%s' % (k, v))

View File

@ -16,5 +16,6 @@
import sys import sys
def is_python3(): def is_python3():
return sys.version_info[0] == 3 return sys.version_info[0] == 3

218
repo
View File

@ -10,6 +10,7 @@ copy of repo in the checkout.
from __future__ import print_function from __future__ import print_function
import datetime
import os import os
import platform import platform
import subprocess import subprocess
@ -24,7 +25,7 @@ def exec_command(cmd):
sys.exit(ret) sys.exit(ret)
else: else:
os.execvp(cmd[0], cmd) os.execvp(cmd[0], cmd)
except: except Exception:
pass pass
@ -113,7 +114,7 @@ if not REPO_REV:
# limitations under the License. # limitations under the License.
# increment this whenever we make important changes to this script # increment this whenever we make important changes to this script
VERSION = (2, 0) VERSION = (2, 3)
# increment this if the MAINTAINER_KEYS block is modified # increment this if the MAINTAINER_KEYS block is modified
KEYRING_VERSION = (2, 0) KEYRING_VERSION = (2, 0)
@ -166,7 +167,12 @@ TACbBS+Up3RpfYVfd63c1cDdlru13pQAn3NQy/SN858MkxN+zym86UBgOad2
""" """
GIT = 'git' # our git command GIT = 'git' # our git command
MIN_GIT_VERSION = (2, 10, 2) # minimum supported git version # NB: The version of git that the repo launcher requires may be much older than
# the version of git that the main repo source tree requires. Keeping this at
# an older version also makes it easier for users to upgrade/rollback as needed.
#
# git-1.7 is in (EOL) Ubuntu Precise.
MIN_GIT_VERSION = (1, 7, 2) # minimum supported git version
repodir = '.repo' # name of repo's private directory repodir = '.repo' # name of repo's private directory
S_repo = 'repo' # special repo repository S_repo = 'repo' # special repo repository
S_manifests = 'manifests' # special manifest repository S_manifests = 'manifests' # special manifest repository
@ -199,88 +205,73 @@ gpg_dir = os.path.join(home_dot_repo, 'gnupg')
extra_args = [] extra_args = []
init_optparse = optparse.OptionParser(usage="repo init -u url [options]") init_optparse = optparse.OptionParser(usage="repo init -u url [options]")
# Logging def _InitParser():
group = init_optparse.add_option_group('Logging options') """Setup the init subcommand parser."""
group.add_option('-q', '--quiet', # Logging.
dest="quiet", action="store_true", default=False, group = init_optparse.add_option_group('Logging options')
help="be quiet") group.add_option('-q', '--quiet',
action='store_true', default=False,
help='be quiet')
# Manifest # Manifest.
group = init_optparse.add_option_group('Manifest options') group = init_optparse.add_option_group('Manifest options')
group.add_option('-u', '--manifest-url', group.add_option('-u', '--manifest-url',
dest='manifest_url', help='manifest repository location', metavar='URL')
help='manifest repository location', metavar='URL') group.add_option('-b', '--manifest-branch',
group.add_option('-b', '--manifest-branch', help='manifest branch or revision', metavar='REVISION')
dest='manifest_branch', group.add_option('-m', '--manifest-name',
help='manifest branch or revision', metavar='REVISION') help='initial manifest file', metavar='NAME.xml')
group.add_option('-m', '--manifest-name', group.add_option('--current-branch',
dest='manifest_name', dest='current_branch_only', action='store_true',
help='initial manifest file', metavar='NAME.xml') help='fetch only current manifest branch from server')
group.add_option('--current-branch', group.add_option('--mirror', action='store_true',
dest='current_branch_only', action='store_true', help='create a replica of the remote repositories '
help='fetch only current manifest branch from server') 'rather than a client working directory')
group.add_option('--mirror', group.add_option('--reference',
dest='mirror', action='store_true', help='location of mirror directory', metavar='DIR')
help='create a replica of the remote repositories ' group.add_option('--dissociate', action='store_true',
'rather than a client working directory') help='dissociate from reference mirrors after clone')
group.add_option('--reference', group.add_option('--depth', type='int', default=None,
dest='reference', help='create a shallow clone with given depth; '
help='location of mirror directory', metavar='DIR') 'see git clone')
group.add_option('--dissociate', group.add_option('--partial-clone', action='store_true',
dest='dissociate', action='store_true', help='perform partial clone (https://git-scm.com/'
help='dissociate from reference mirrors after clone') 'docs/gitrepository-layout#_code_partialclone_code)')
group.add_option('--depth', type='int', default=None, group.add_option('--clone-filter', action='store', default='blob:none',
dest='depth', help='filter for use with --partial-clone '
help='create a shallow clone with given depth; see git clone') '[default: %default]')
group.add_option('--partial-clone', action='store_true', group.add_option('--archive', action='store_true',
dest='partial_clone', help='checkout an archive instead of a git repository for '
help='perform partial clone (https://git-scm.com/' 'each project. See git archive.')
'docs/gitrepository-layout#_code_partialclone_code)') group.add_option('--submodules', action='store_true',
group.add_option('--clone-filter', action='store', default='blob:none', help='sync any submodules associated with the manifest repo')
dest='clone_filter', group.add_option('-g', '--groups', default='default',
help='filter for use with --partial-clone [default: %default]') help='restrict manifest projects to ones with specified '
group.add_option('--archive', 'group(s) [default|all|G1,G2,G3|G4,-G5,-G6]',
dest='archive', action='store_true', metavar='GROUP')
help='checkout an archive instead of a git repository for ' group.add_option('-p', '--platform', default='auto',
'each project. See git archive.') help='restrict manifest projects to ones with a specified '
group.add_option('--submodules', 'platform group [auto|all|none|linux|darwin|...]',
dest='submodules', action='store_true', metavar='PLATFORM')
help='sync any submodules associated with the manifest repo') group.add_option('--no-clone-bundle', action='store_true',
group.add_option('-g', '--groups', help='disable use of /clone.bundle on HTTP/HTTPS')
dest='groups', default='default', group.add_option('--no-tags', action='store_true',
help='restrict manifest projects to ones with specified ' help="don't fetch tags in the manifest")
'group(s) [default|all|G1,G2,G3|G4,-G5,-G6]',
metavar='GROUP')
group.add_option('-p', '--platform',
dest='platform', default="auto",
help='restrict manifest projects to ones with a specified '
'platform group [auto|all|none|linux|darwin|...]',
metavar='PLATFORM')
group.add_option('--no-clone-bundle',
dest='no_clone_bundle', action='store_true',
help='disable use of /clone.bundle on HTTP/HTTPS')
group.add_option('--no-tags',
dest='no_tags', action='store_true',
help="don't fetch tags in the manifest")
# Tool.
group = init_optparse.add_option_group('repo Version options')
group.add_option('--repo-url', metavar='URL',
help='repo repository location ($REPO_URL)')
group.add_option('--repo-branch', metavar='REVISION',
help='repo branch or revision ($REPO_REV)')
group.add_option('--no-repo-verify', action='store_true',
help='do not verify repo source code')
# Tool # Other.
group = init_optparse.add_option_group('repo Version options') group = init_optparse.add_option_group('Other options')
group.add_option('--repo-url', group.add_option('--config-name',
dest='repo_url', action='store_true', default=False,
help='repo repository location ($REPO_URL)', metavar='URL') help='Always prompt for name/e-mail')
group.add_option('--repo-branch',
dest='repo_branch',
help='repo branch or revision ($REPO_REV)', metavar='REVISION')
group.add_option('--no-repo-verify',
dest='no_repo_verify', action='store_true',
help='do not verify repo source code')
# Other
group = init_optparse.add_option_group('Other options')
group.add_option('--config-name',
dest='config_name', action="store_true", default=False,
help='Always prompt for name/e-mail')
def _GitcInitOptions(init_optparse_arg): def _GitcInitOptions(init_optparse_arg):
@ -488,6 +479,39 @@ def _CheckGitVersion():
raise CloneFailure() raise CloneFailure()
def SetGitTrace2ParentSid(env=None):
"""Set up GIT_TRACE2_PARENT_SID for git tracing."""
# We roughly follow the format git itself uses in trace2/tr2_sid.c.
# (1) Be unique (2) be valid filename (3) be fixed length.
#
# Since we always export this variable, we try to avoid more expensive calls.
# e.g. We don't attempt hostname lookups or hashing the results.
if env is None:
env = os.environ
KEY = 'GIT_TRACE2_PARENT_SID'
now = datetime.datetime.utcnow()
value = 'repo-%s-P%08x' % (now.strftime('%Y%m%dT%H%M%SZ'), os.getpid())
# If it's already set, then append ourselves.
if KEY in env:
value = env[KEY] + '/' + value
_setenv(KEY, value, env=env)
def _setenv(key, value, env=None):
"""Set |key| in the OS environment |env| to |value|."""
if env is None:
env = os.environ
# Environment handling across systems is messy.
try:
env[key] = value
except UnicodeEncodeError:
env[key] = value.encode()
def NeedSetupGnuPG(): def NeedSetupGnuPG():
if not os.path.isdir(home_dot_repo): if not os.path.isdir(home_dot_repo):
return True return True
@ -524,10 +548,7 @@ def SetupGnuPG(quiet):
sys.exit(1) sys.exit(1)
env = os.environ.copy() env = os.environ.copy()
try: _setenv('GNUPGHOME', gpg_dir, env)
env['GNUPGHOME'] = gpg_dir
except UnicodeEncodeError:
env['GNUPGHOME'] = gpg_dir.encode()
cmd = ['gpg', '--import'] cmd = ['gpg', '--import']
try: try:
@ -733,10 +754,7 @@ def _Verify(cwd, branch, quiet):
print(file=sys.stderr) print(file=sys.stderr)
env = os.environ.copy() env = os.environ.copy()
try: _setenv('GNUPGHOME', gpg_dir, env)
env['GNUPGHOME'] = gpg_dir
except UnicodeEncodeError:
env['GNUPGHOME'] = gpg_dir.encode()
cmd = [GIT, 'tag', '-v', cur] cmd = [GIT, 'tag', '-v', cur]
proc = subprocess.Popen(cmd, proc = subprocess.Popen(cmd,
@ -801,6 +819,7 @@ def _FindRepo():
class _Options(object): class _Options(object):
help = False help = False
version = False
def _ParseArguments(args): def _ParseArguments(args):
@ -812,7 +831,8 @@ def _ParseArguments(args):
a = args[i] a = args[i]
if a == '-h' or a == '--help': if a == '-h' or a == '--help':
opt.help = True opt.help = True
elif a == '--version':
opt.version = True
elif not a.startswith('-'): elif not a.startswith('-'):
cmd = a cmd = a
arg = args[i + 1:] arg = args[i + 1:]
@ -859,6 +879,16 @@ def _Help(args):
sys.exit(1) sys.exit(1)
def _Version():
"""Show version information."""
print('<repo not installed>')
print('repo launcher version %s' % ('.'.join(str(x) for x in VERSION),))
print(' (from %s)' % (__file__,))
print('git %s' % (ParseGitVersion().full,))
print('Python %s' % sys.version)
sys.exit(0)
def _NotInstalled(): def _NotInstalled():
print('error: repo is not installed. Use "repo init" to install it here.', print('error: repo is not installed. Use "repo init" to install it here.',
file=sys.stderr) file=sys.stderr)
@ -911,6 +941,9 @@ def _SetDefaultsTo(gitdir):
def main(orig_args): def main(orig_args):
cmd, opt, args = _ParseArguments(orig_args) cmd, opt, args = _ParseArguments(orig_args)
# We run this early as we run some git commands ourselves.
SetGitTrace2ParentSid()
repo_main, rel_repo_dir = None, None repo_main, rel_repo_dir = None, None
# Don't use the local repo copy, make sure to switch to the gitc client first. # Don't use the local repo copy, make sure to switch to the gitc client first.
if cmd != 'gitc-init': if cmd != 'gitc-init':
@ -926,11 +959,14 @@ def main(orig_args):
'command from the corresponding client under /gitc/', 'command from the corresponding client under /gitc/',
file=sys.stderr) file=sys.stderr)
sys.exit(1) sys.exit(1)
_InitParser()
if not repo_main: if not repo_main:
if opt.help: if opt.help:
_Usage() _Usage()
if cmd == 'help': if cmd == 'help':
_Help(args) _Help(args)
if opt.version or cmd == 'version':
_Version()
if not cmd: if not cmd:
_NotInstalled() _NotInstalled()
if cmd == 'init' or cmd == 'gitc-init': if cmd == 'init' or cmd == 'gitc-init':

View File

@ -28,13 +28,16 @@ REPO_TRACE = 'REPO_TRACE'
_TRACE = os.environ.get(REPO_TRACE) == '1' _TRACE = os.environ.get(REPO_TRACE) == '1'
def IsTrace(): def IsTrace():
return _TRACE return _TRACE
def SetTrace(): def SetTrace():
global _TRACE global _TRACE
_TRACE = True _TRACE = True
def Trace(fmt, *args): def Trace(fmt, *args):
if IsTrace(): if IsTrace():
print(fmt % args, file=sys.stderr) print(fmt % args, file=sys.stderr)

View File

@ -40,7 +40,7 @@ for py in os.listdir(my_dir):
cmd = getattr(mod, clsn)() cmd = getattr(mod, clsn)()
except AttributeError: except AttributeError:
raise SyntaxError('%s/%s does not define class %s' % ( raise SyntaxError('%s/%s does not define class %s' % (
__name__, py, clsn)) __name__, py, clsn))
name = name.replace('_', '-') name = name.replace('_', '-')
cmd.NAME = name cmd.NAME = name

View File

@ -21,6 +21,7 @@ from collections import defaultdict
from git_command import git from git_command import git
from progress import Progress from progress import Progress
class Abandon(Command): class Abandon(Command):
common = True common = True
helpSummary = "Permanently abandon a development branch" helpSummary = "Permanently abandon a development branch"
@ -32,6 +33,7 @@ deleting it (and all its history) from your local repository.
It is equivalent to "git branch -D <branchname>". It is equivalent to "git branch -D <branchname>".
""" """
def _Options(self, p): def _Options(self, p):
p.add_option('--all', p.add_option('--all',
dest='all', action='store_true', dest='all', action='store_true',
@ -79,10 +81,10 @@ It is equivalent to "git branch -D <branchname>".
if err: if err:
for br in err.keys(): for br in err.keys():
err_msg = "error: cannot abandon %s" %br err_msg = "error: cannot abandon %s" % br
print(err_msg, file=sys.stderr) print(err_msg, file=sys.stderr)
for proj in err[br]: for proj in err[br]:
print(' '*len(err_msg) + " | %s" % proj.relpath, file=sys.stderr) print(' ' * len(err_msg) + " | %s" % proj.relpath, file=sys.stderr)
sys.exit(1) sys.exit(1)
elif not success: elif not success:
print('error: no project has local branch(es) : %s' % nb, print('error: no project has local branch(es) : %s' % nb,
@ -95,5 +97,5 @@ It is equivalent to "git branch -D <branchname>".
result = "all project" result = "all project"
else: else:
result = "%s" % ( result = "%s" % (
('\n'+' '*width + '| ').join(p.relpath for p in success[br])) ('\n' + ' ' * width + '| ').join(p.relpath for p in success[br]))
print("%s%s| %s\n" % (br,' '*(width-len(br)), result),file=sys.stderr) print("%s%s| %s\n" % (br, ' ' * (width - len(br)), result), file=sys.stderr)

View File

@ -19,13 +19,15 @@ import sys
from color import Coloring from color import Coloring
from command import Command from command import Command
class BranchColoring(Coloring): class BranchColoring(Coloring):
def __init__(self, config): def __init__(self, config):
Coloring.__init__(self, config, 'branch') Coloring.__init__(self, config, 'branch')
self.current = self.printer('current', fg='green') self.current = self.printer('current', fg='green')
self.local = self.printer('local') self.local = self.printer('local')
self.notinproject = self.printer('notinproject', fg='red') self.notinproject = self.printer('notinproject', fg='red')
class BranchInfo(object): class BranchInfo(object):
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name
@ -158,7 +160,7 @@ is shown, then the branch appears in all projects.
for b in i.projects: for b in i.projects:
have.add(b.project) have.add(b.project)
for p in projects: for p in projects:
if not p in have: if p not in have:
paths.append(p.relpath) paths.append(p.relpath)
s = ' %s %s' % (in_type, ', '.join(paths)) s = ' %s %s' % (in_type, ', '.join(paths))
@ -170,11 +172,11 @@ is shown, then the branch appears in all projects.
fmt = out.current if i.IsCurrent else out.write fmt = out.current if i.IsCurrent else out.write
for p in paths: for p in paths:
out.nl() out.nl()
fmt(width*' ' + ' %s' % p) fmt(width * ' ' + ' %s' % p)
fmt = out.write fmt = out.write
for p in non_cur_paths: for p in non_cur_paths:
out.nl() out.nl()
fmt(width*' ' + ' %s' % p) fmt(width * ' ' + ' %s' % p)
else: else:
out.write(' in all projects') out.write(' in all projects')
out.nl() out.nl()

View File

@ -19,6 +19,7 @@ import sys
from command import Command from command import Command
from progress import Progress from progress import Progress
class Checkout(Command): class Checkout(Command):
common = True common = True
helpSummary = "Checkout a branch for development" helpSummary = "Checkout a branch for development"

View File

@ -22,6 +22,7 @@ from git_command import GitCommand
CHANGE_ID_RE = re.compile(r'^\s*Change-Id: I([0-9a-f]{40})\s*$') CHANGE_ID_RE = re.compile(r'^\s*Change-Id: I([0-9a-f]{40})\s*$')
class CherryPick(Command): class CherryPick(Command):
common = True common = True
helpSummary = "Cherry-pick a change." helpSummary = "Cherry-pick a change."
@ -46,8 +47,8 @@ change id will be added.
p = GitCommand(None, p = GitCommand(None,
['rev-parse', '--verify', reference], ['rev-parse', '--verify', reference],
capture_stdout = True, capture_stdout=True,
capture_stderr = True) capture_stderr=True)
if p.Wait() != 0: if p.Wait() != 0:
print(p.stderr, file=sys.stderr) print(p.stderr, file=sys.stderr)
sys.exit(1) sys.exit(1)
@ -61,8 +62,8 @@ change id will be added.
p = GitCommand(None, p = GitCommand(None,
['cherry-pick', sha1], ['cherry-pick', sha1],
capture_stdout = True, capture_stdout=True,
capture_stderr = True) capture_stderr=True)
status = p.Wait() status = p.Wait()
print(p.stdout, file=sys.stdout) print(p.stdout, file=sys.stdout)
@ -74,9 +75,9 @@ change id will be added.
new_msg = self._Reformat(old_msg, sha1) new_msg = self._Reformat(old_msg, sha1)
p = GitCommand(None, ['commit', '--amend', '-F', '-'], p = GitCommand(None, ['commit', '--amend', '-F', '-'],
provide_stdin = True, provide_stdin=True,
capture_stdout = True, capture_stdout=True,
capture_stderr = True) capture_stderr=True)
p.stdin.write(new_msg) p.stdin.write(new_msg)
p.stdin.close() p.stdin.close()
if p.Wait() != 0: if p.Wait() != 0:
@ -97,7 +98,7 @@ change id will be added.
def _StripHeader(self, commit_msg): def _StripHeader(self, commit_msg):
lines = commit_msg.splitlines() lines = commit_msg.splitlines()
return "\n".join(lines[lines.index("")+1:]) return "\n".join(lines[lines.index("") + 1:])
def _Reformat(self, old_msg, sha1): def _Reformat(self, old_msg, sha1):
new_msg = [] new_msg = []

View File

@ -16,6 +16,7 @@
from command import PagedCommand from command import PagedCommand
class Diff(PagedCommand): class Diff(PagedCommand):
common = True common = True
helpSummary = "Show changes between commit and working tree" helpSummary = "Show changes between commit and working tree"

View File

@ -18,10 +18,12 @@ from color import Coloring
from command import PagedCommand from command import PagedCommand
from manifest_xml import XmlManifest from manifest_xml import XmlManifest
class _Coloring(Coloring): class _Coloring(Coloring):
def __init__(self, config): def __init__(self, config):
Coloring.__init__(self, config, "status") Coloring.__init__(self, config, "status")
class Diffmanifests(PagedCommand): class Diffmanifests(PagedCommand):
""" A command to see logs in projects represented by manifests """ A command to see logs in projects represented by manifests
@ -184,10 +186,10 @@ synced and their revisions won't be found.
self.out = _Coloring(self.manifest.globalConfig) self.out = _Coloring(self.manifest.globalConfig)
self.printText = self.out.nofmt_printer('text') self.printText = self.out.nofmt_printer('text')
if opt.color: if opt.color:
self.printProject = self.out.nofmt_printer('project', attr = 'bold') self.printProject = self.out.nofmt_printer('project', attr='bold')
self.printAdded = self.out.nofmt_printer('green', fg = 'green', attr = 'bold') self.printAdded = self.out.nofmt_printer('green', fg='green', attr='bold')
self.printRemoved = self.out.nofmt_printer('red', fg = 'red', attr = 'bold') self.printRemoved = self.out.nofmt_printer('red', fg='red', attr='bold')
self.printRevision = self.out.nofmt_printer('revision', fg = 'yellow') self.printRevision = self.out.nofmt_printer('revision', fg='yellow')
else: else:
self.printProject = self.printAdded = self.printRemoved = self.printRevision = self.printText self.printProject = self.printAdded = self.printRemoved = self.printRevision = self.printText

View File

@ -23,6 +23,7 @@ from error import GitError
CHANGE_RE = re.compile(r'^([1-9][0-9]*)(?:[/\.-]([1-9][0-9]*))?$') CHANGE_RE = re.compile(r'^([1-9][0-9]*)(?:[/\.-]([1-9][0-9]*))?$')
class Download(Command): class Download(Command):
common = True common = True
helpSummary = "Download and checkout a change" helpSummary = "Download and checkout a change"
@ -93,7 +94,7 @@ If no project is specified try to use current directory as a project.
continue continue
if len(dl.commits) > 1: if len(dl.commits) > 1:
print('[%s] %d/%d depends on %d unmerged changes:' \ print('[%s] %d/%d depends on %d unmerged changes:'
% (project.name, change_id, ps_id, len(dl.commits)), % (project.name, change_id, ps_id, len(dl.commits)),
file=sys.stderr) file=sys.stderr)
for c in dl.commits: for c in dl.commits:
@ -102,7 +103,7 @@ If no project is specified try to use current directory as a project.
try: try:
project._CherryPick(dl.commit) project._CherryPick(dl.commit)
except GitError: except GitError:
print('[%s] Could not complete the cherry-pick of %s' \ print('[%s] Could not complete the cherry-pick of %s'
% (project.name, dl.commit), file=sys.stderr) % (project.name, dl.commit), file=sys.stderr)
sys.exit(1) sys.exit(1)

View File

@ -28,10 +28,10 @@ from command import Command, MirrorSafeCommand
import platform_utils import platform_utils
_CAN_COLOR = [ _CAN_COLOR = [
'branch', 'branch',
'diff', 'diff',
'grep', 'grep',
'log', 'log',
] ]
@ -170,14 +170,14 @@ without iterating through the remaining projects.
else: else:
lrev = None lrev = None
return { return {
'name': project.name, 'name': project.name,
'relpath': project.relpath, 'relpath': project.relpath,
'remote_name': project.remote.name, 'remote_name': project.remote.name,
'lrev': lrev, 'lrev': lrev,
'rrev': project.revisionExpr, 'rrev': project.revisionExpr,
'annotations': dict((a.name, a.value) for a in project.annotations), 'annotations': dict((a.name, a.value) for a in project.annotations),
'gitdir': project.gitdir, 'gitdir': project.gitdir,
'worktree': project.worktree, 'worktree': project.worktree,
} }
def ValidateOptions(self, opt, args): def ValidateOptions(self, opt, args):
@ -195,9 +195,9 @@ without iterating through the remaining projects.
cmd.append(cmd[0]) cmd.append(cmd[0])
cmd.extend(opt.command[1:]) cmd.extend(opt.command[1:])
if opt.project_header \ if opt.project_header \
and not shell \ and not shell \
and cmd[0] == 'git': and cmd[0] == 'git':
# If this is a direct git command that can enable colorized # If this is a direct git command that can enable colorized
# output and the user prefers coloring, add --color into the # output and the user prefers coloring, add --color into the
# command line because we are going to wrap the command into # command line because we are going to wrap the command into
@ -220,7 +220,7 @@ without iterating through the remaining projects.
smart_sync_manifest_name = "smart_sync_override.xml" smart_sync_manifest_name = "smart_sync_override.xml"
smart_sync_manifest_path = os.path.join( smart_sync_manifest_path = os.path.join(
self.manifest.manifestProject.worktree, smart_sync_manifest_name) self.manifest.manifestProject.worktree, smart_sync_manifest_name)
if os.path.isfile(smart_sync_manifest_path): if os.path.isfile(smart_sync_manifest_path):
self.manifest.Override(smart_sync_manifest_path) self.manifest.Override(smart_sync_manifest_path)
@ -238,8 +238,8 @@ without iterating through the remaining projects.
try: try:
config = self.manifest.manifestProject.config config = self.manifest.manifestProject.config
results_it = pool.imap( results_it = pool.imap(
DoWorkWrapper, DoWorkWrapper,
self.ProjectArgs(projects, mirror, opt, cmd, shell, config)) self.ProjectArgs(projects, mirror, opt, cmd, shell, config))
pool.close() pool.close()
for r in results_it: for r in results_it:
rc = rc or r rc = rc or r
@ -253,7 +253,7 @@ without iterating through the remaining projects.
except Exception as e: except Exception as e:
# Catch any other exceptions raised # Catch any other exceptions raised
print('Got an error, terminating the pool: %s: %s' % print('Got an error, terminating the pool: %s: %s' %
(type(e).__name__, e), (type(e).__name__, e),
file=sys.stderr) file=sys.stderr)
pool.terminate() pool.terminate()
rc = rc or getattr(e, 'errno', 1) rc = rc or getattr(e, 'errno', 1)
@ -268,7 +268,7 @@ without iterating through the remaining projects.
project = self._SerializeProject(p) project = self._SerializeProject(p)
except Exception as e: except Exception as e:
print('Project list error on project %s: %s: %s' % print('Project list error on project %s: %s: %s' %
(p.name, type(e).__name__, e), (p.name, type(e).__name__, e),
file=sys.stderr) file=sys.stderr)
return return
except KeyboardInterrupt: except KeyboardInterrupt:
@ -277,6 +277,7 @@ without iterating through the remaining projects.
return return
yield [mirror, opt, cmd, shell, cnt, config, project] yield [mirror, opt, cmd, shell, cnt, config, project]
class WorkerKeyboardInterrupt(Exception): class WorkerKeyboardInterrupt(Exception):
""" Keyboard interrupt exception for worker processes. """ """ Keyboard interrupt exception for worker processes. """
pass pass
@ -285,6 +286,7 @@ class WorkerKeyboardInterrupt(Exception):
def InitWorker(): def InitWorker():
signal.signal(signal.SIGINT, signal.SIG_IGN) signal.signal(signal.SIGINT, signal.SIG_IGN)
def DoWorkWrapper(args): def DoWorkWrapper(args):
""" A wrapper around the DoWork() method. """ A wrapper around the DoWork() method.
@ -303,6 +305,7 @@ def DoWorkWrapper(args):
def DoWork(project, mirror, opt, cmd, shell, cnt, config): def DoWork(project, mirror, opt, cmd, shell, cnt, config):
env = os.environ.copy() env = os.environ.copy()
def setenv(name, val): def setenv(name, val):
if val is None: if val is None:
val = '' val = ''
@ -331,7 +334,7 @@ def DoWork(project, mirror, opt, cmd, shell, cnt, config):
if opt.ignore_missing: if opt.ignore_missing:
return 0 return 0
if ((opt.project_header and opt.verbose) if ((opt.project_header and opt.verbose)
or not opt.project_header): or not opt.project_header):
print('skipping %s/' % project['relpath'], file=sys.stderr) print('skipping %s/' % project['relpath'], file=sys.stderr)
return 1 return 1

View File

@ -24,6 +24,7 @@ from pyversion import is_python3
if not is_python3(): if not is_python3():
input = raw_input input = raw_input
class GitcDelete(Command, GitcClientCommand): class GitcDelete(Command, GitcClientCommand):
common = True common = True
visible_everywhere = False visible_everywhere = False

View File

@ -21,7 +21,8 @@ import sys
from color import Coloring from color import Coloring
from command import PagedCommand from command import PagedCommand
from error import GitError from error import GitError
from git_command import git_require, GitCommand from git_command import GitCommand
class GrepColoring(Coloring): class GrepColoring(Coloring):
def __init__(self, config): def __init__(self, config):
@ -29,6 +30,7 @@ class GrepColoring(Coloring):
self.project = self.printer('project', attr='bold') self.project = self.printer('project', attr='bold')
self.fail = self.printer('fail', fg='red') self.fail = self.printer('fail', fg='red')
class Grep(PagedCommand): class Grep(PagedCommand):
common = True common = True
helpSummary = "Print lines matching a pattern" helpSummary = "Print lines matching a pattern"
@ -156,12 +158,11 @@ contain a line that matches both expressions:
action='callback', callback=carry, action='callback', callback=carry,
help='Show only file names not containing matching lines') help='Show only file names not containing matching lines')
def Execute(self, opt, args): def Execute(self, opt, args):
out = GrepColoring(self.manifest.manifestProject.config) out = GrepColoring(self.manifest.manifestProject.config)
cmd_argv = ['grep'] cmd_argv = ['grep']
if out.is_on and git_require((1, 6, 3)): if out.is_on:
cmd_argv.append('--color') cmd_argv.append('--color')
cmd_argv.extend(getattr(opt, 'cmd_argv', [])) cmd_argv.extend(getattr(opt, 'cmd_argv', []))

View File

@ -23,6 +23,7 @@ from color import Coloring
from command import PagedCommand, MirrorSafeCommand, GitcAvailableCommand, GitcClientCommand from command import PagedCommand, MirrorSafeCommand, GitcAvailableCommand, GitcClientCommand
import gitc_utils import gitc_utils
class Help(PagedCommand, MirrorSafeCommand): class Help(PagedCommand, MirrorSafeCommand):
common = False common = False
helpSummary = "Display detailed help on a command" helpSummary = "Display detailed help on a command"
@ -72,13 +73,13 @@ Displays detailed usage information about a command.
return False return False
commandNames = list(sorted([name commandNames = list(sorted([name
for name, command in self.commands.items() for name, command in self.commands.items()
if command.common and gitc_supported(command)])) if command.common and gitc_supported(command)]))
self._PrintCommands(commandNames) self._PrintCommands(commandNames)
print( print(
"See 'repo help <command>' for more information on a specific command.\n" "See 'repo help <command>' for more information on a specific command.\n"
"See 'repo help --all' for a complete list of recognized commands.") "See 'repo help --all' for a complete list of recognized commands.")
def _PrintCommandHelp(self, cmd, header_prefix=''): def _PrintCommandHelp(self, cmd, header_prefix=''):
class _Out(Coloring): class _Out(Coloring):

View File

@ -18,10 +18,12 @@ from command import PagedCommand
from color import Coloring from color import Coloring
from git_refs import R_M from git_refs import R_M
class _Coloring(Coloring): class _Coloring(Coloring):
def __init__(self, config): def __init__(self, config):
Coloring.__init__(self, config, "status") Coloring.__init__(self, config, "status")
class Info(PagedCommand): class Info(PagedCommand):
common = True common = True
helpSummary = "Get info on the manifest branch, current branch or unmerged branches" helpSummary = "Get info on the manifest branch, current branch or unmerged branches"
@ -41,15 +43,14 @@ class Info(PagedCommand):
dest="local", action="store_true", dest="local", action="store_true",
help="Disable all remote operations") help="Disable all remote operations")
def Execute(self, opt, args): def Execute(self, opt, args):
self.out = _Coloring(self.manifest.globalConfig) self.out = _Coloring(self.manifest.globalConfig)
self.heading = self.out.printer('heading', attr = 'bold') self.heading = self.out.printer('heading', attr='bold')
self.headtext = self.out.nofmt_printer('headtext', fg = 'yellow') self.headtext = self.out.nofmt_printer('headtext', fg='yellow')
self.redtext = self.out.printer('redtext', fg = 'red') self.redtext = self.out.printer('redtext', fg='red')
self.sha = self.out.printer("sha", fg = 'yellow') self.sha = self.out.printer("sha", fg='yellow')
self.text = self.out.nofmt_printer('text') self.text = self.out.nofmt_printer('text')
self.dimtext = self.out.printer('dimtext', attr = 'dim') self.dimtext = self.out.printer('dimtext', attr='dim')
self.opt = opt self.opt = opt
@ -122,7 +123,7 @@ class Info(PagedCommand):
self.printSeparator() self.printSeparator()
def findRemoteLocalDiff(self, project): def findRemoteLocalDiff(self, project):
#Fetch all the latest commits # Fetch all the latest commits.
if not self.opt.local: if not self.opt.local:
project.Sync_NetworkHalf(quiet=True, current_branch_only=True) project.Sync_NetworkHalf(quiet=True, current_branch_only=True)
@ -195,16 +196,16 @@ class Info(PagedCommand):
commits = branch.commits commits = branch.commits
date = branch.date date = branch.date
self.text('%s %-33s (%2d commit%s, %s)' % ( self.text('%s %-33s (%2d commit%s, %s)' % (
branch.name == project.CurrentBranch and '*' or ' ', branch.name == project.CurrentBranch and '*' or ' ',
branch.name, branch.name,
len(commits), len(commits),
len(commits) != 1 and 's' or '', len(commits) != 1 and 's' or '',
date)) date))
self.out.nl() self.out.nl()
for commit in commits: for commit in commits:
split = commit.split() split = commit.split()
self.text('{0:38}{1} '.format('','-')) self.text('{0:38}{1} '.format('', '-'))
self.sha(split[0] + " ") self.sha(split[0] + " ")
self.text(" ".join(split[1:])) self.text(" ".join(split[1:]))
self.out.nl() self.out.nl()

View File

@ -34,9 +34,10 @@ from command import InteractiveCommand, MirrorSafeCommand
from error import ManifestParseError from error import ManifestParseError
from project import SyncBuffer from project import SyncBuffer
from git_config import GitConfig from git_config import GitConfig
from git_command import git_require, MIN_GIT_VERSION from git_command import git_require, MIN_GIT_VERSION_SOFT, MIN_GIT_VERSION_HARD
import platform_utils import platform_utils
class Init(InteractiveCommand, MirrorSafeCommand): class Init(InteractiveCommand, MirrorSafeCommand):
common = True common = True
helpSummary = "Initialize repo in the current directory" helpSummary = "Initialize repo in the current directory"
@ -223,7 +224,7 @@ to update the working directory files.
platformize = lambda x: 'platform-' + x platformize = lambda x: 'platform-' + x
if opt.platform == 'auto': if opt.platform == 'auto':
if (not opt.mirror and if (not opt.mirror and
not m.config.GetString('repo.mirror') == 'true'): not m.config.GetString('repo.mirror') == 'true'):
groups.append(platformize(platform.system().lower())) groups.append(platformize(platform.system().lower()))
elif opt.platform == 'all': elif opt.platform == 'all':
groups.extend(map(platformize, all_platforms)) groups.extend(map(platformize, all_platforms))
@ -280,10 +281,10 @@ to update the working directory files.
m.config.SetString('repo.submodules', 'true') m.config.SetString('repo.submodules', 'true')
if not m.Sync_NetworkHalf(is_new=is_new, quiet=opt.quiet, if not m.Sync_NetworkHalf(is_new=is_new, quiet=opt.quiet,
clone_bundle=not opt.no_clone_bundle, clone_bundle=not opt.no_clone_bundle,
current_branch_only=opt.current_branch_only, current_branch_only=opt.current_branch_only,
no_tags=opt.no_tags, submodules=opt.submodules, no_tags=opt.no_tags, submodules=opt.submodules,
clone_filter=opt.clone_filter): clone_filter=opt.clone_filter):
r = m.GetRemote(m.remote.name) r = m.GetRemote(m.remote.name)
print('fatal: cannot obtain manifest %s' % r.url, file=sys.stderr) print('fatal: cannot obtain manifest %s' % r.url, file=sys.stderr)
@ -349,7 +350,7 @@ to update the working directory files.
while True: while True:
print() print()
name = self._Prompt('Your Name', mp.UserName) name = self._Prompt('Your Name', mp.UserName)
email = self._Prompt('Your Email', mp.UserEmail) email = self._Prompt('Your Email', mp.UserEmail)
print() print()
@ -451,7 +452,12 @@ to update the working directory files.
self.OptionParser.error('--mirror and --archive cannot be used together.') self.OptionParser.error('--mirror and --archive cannot be used together.')
def Execute(self, opt, args): def Execute(self, opt, args):
git_require(MIN_GIT_VERSION, fail=True) git_require(MIN_GIT_VERSION_HARD, fail=True)
if not git_require(MIN_GIT_VERSION_SOFT):
print('repo: warning: git-%s+ will soon be required; please upgrade your '
'version of git to maintain support.'
% ('.'.join(str(x) for x in MIN_GIT_VERSION_SOFT),),
file=sys.stderr)
self._SyncManifest(opt) self._SyncManifest(opt)
self._LinkManifest(opt.manifest_name) self._LinkManifest(opt.manifest_name)

View File

@ -15,10 +15,10 @@
# limitations under the License. # limitations under the License.
from __future__ import print_function from __future__ import print_function
import sys
from command import Command, MirrorSafeCommand from command import Command, MirrorSafeCommand
class List(Command, MirrorSafeCommand): class List(Command, MirrorSafeCommand):
common = True common = True
helpSummary = "List projects and their associated directories" helpSummary = "List projects and their associated directories"
@ -77,7 +77,7 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
lines = [] lines = []
for project in projects: for project in projects:
if opt.name_only and not opt.path_only: if opt.name_only and not opt.path_only:
lines.append("%s" % ( project.name)) lines.append("%s" % (project.name))
elif opt.path_only and not opt.name_only: elif opt.path_only and not opt.name_only:
lines.append("%s" % (_getpath(project))) lines.append("%s" % (_getpath(project)))
else: else:

View File

@ -20,6 +20,7 @@ import sys
from command import PagedCommand from command import PagedCommand
class Manifest(PagedCommand): class Manifest(PagedCommand):
common = False common = False
helpSummary = "Manifest inspection utility" helpSummary = "Manifest inspection utility"
@ -66,8 +67,8 @@ in a Git repository for use during future 'repo init' invocations.
else: else:
fd = open(opt.output_file, 'w') fd = open(opt.output_file, 'w')
self.manifest.Save(fd, self.manifest.Save(fd,
peg_rev = opt.peg_rev, peg_rev=opt.peg_rev,
peg_rev_upstream = opt.peg_rev_upstream) peg_rev_upstream=opt.peg_rev_upstream)
fd.close() fd.close()
if opt.output_file != '-': if opt.output_file != '-':
print('Saved manifest to %s' % opt.output_file, file=sys.stderr) print('Saved manifest to %s' % opt.output_file, file=sys.stderr)

View File

@ -18,6 +18,7 @@ from __future__ import print_function
from color import Coloring from color import Coloring
from command import PagedCommand from command import PagedCommand
class Prune(PagedCommand): class Prune(PagedCommand):
common = True common = True
helpSummary = "Prune (delete) already merged topics" helpSummary = "Prune (delete) already merged topics"

View File

@ -43,8 +43,8 @@ branch but need to incorporate new upstream changes "underneath" them.
def _Options(self, p): def _Options(self, p):
p.add_option('-i', '--interactive', p.add_option('-i', '--interactive',
dest="interactive", action="store_true", dest="interactive", action="store_true",
help="interactive rebase (single project only)") help="interactive rebase (single project only)")
p.add_option('--fail-fast', p.add_option('--fail-fast',
dest='fail_fast', action='store_true', dest='fail_fast', action='store_true',
@ -82,7 +82,7 @@ branch but need to incorporate new upstream changes "underneath" them.
file=sys.stderr) file=sys.stderr)
if len(args) == 1: if len(args) == 1:
print('note: project %s is mapped to more than one path' % (args[0],), print('note: project %s is mapped to more than one path' % (args[0],),
file=sys.stderr) file=sys.stderr)
return 1 return 1
# Setup the common git rebase args that we use for all projects. # Setup the common git rebase args that we use for all projects.

View File

@ -22,6 +22,7 @@ from command import Command, MirrorSafeCommand
from subcmds.sync import _PostRepoUpgrade from subcmds.sync import _PostRepoUpgrade
from subcmds.sync import _PostRepoFetch from subcmds.sync import _PostRepoFetch
class Selfupdate(Command, MirrorSafeCommand): class Selfupdate(Command, MirrorSafeCommand):
common = False common = False
helpSummary = "Update repo to the latest version" helpSummary = "Update repo to the latest version"
@ -59,5 +60,5 @@ need to be performed by an end-user.
rp.bare_git.gc('--auto') rp.bare_git.gc('--auto')
_PostRepoFetch(rp, _PostRepoFetch(rp,
no_repo_verify = opt.no_repo_verify, no_repo_verify=opt.no_repo_verify,
verbose = True) verbose=True)

View File

@ -16,6 +16,7 @@
from subcmds.sync import Sync from subcmds.sync import Sync
class Smartsync(Sync): class Smartsync(Sync):
common = True common = True
helpSummary = "Update working tree to the latest known good revision" helpSummary = "Update working tree to the latest known good revision"

View File

@ -21,6 +21,7 @@ from color import Coloring
from command import InteractiveCommand from command import InteractiveCommand
from git_command import GitCommand from git_command import GitCommand
class _ProjectList(Coloring): class _ProjectList(Coloring):
def __init__(self, gc): def __init__(self, gc):
Coloring.__init__(self, gc, 'interactive') Coloring.__init__(self, gc, 'interactive')
@ -28,6 +29,7 @@ class _ProjectList(Coloring):
self.header = self.printer('header', attr='bold') self.header = self.printer('header', attr='bold')
self.help = self.printer('help', fg='red', attr='bold') self.help = self.printer('help', fg='red', attr='bold')
class Stage(InteractiveCommand): class Stage(InteractiveCommand):
common = True common = True
helpSummary = "Stage file(s) for commit" helpSummary = "Stage file(s) for commit"
@ -105,6 +107,7 @@ The '%prog' command stages files to prepare the next commit.
continue continue
print('Bye.') print('Bye.')
def _AddI(project): def _AddI(project):
p = GitCommand(project, ['add', '--interactive'], bare=False) p = GitCommand(project, ['add', '--interactive'], bare=False)
p.Wait() p.Wait()

View File

@ -25,6 +25,7 @@ import gitc_utils
from progress import Progress from progress import Progress
from project import SyncBuffer from project import SyncBuffer
class Start(Command): class Start(Command):
common = True common = True
helpSummary = "Start a new branch for development" helpSummary = "Start a new branch for development"
@ -60,7 +61,7 @@ revision specified in the manifest.
if not opt.all: if not opt.all:
projects = args[1:] projects = args[1:]
if len(projects) < 1: if len(projects) < 1:
projects = ['.',] # start it in the local project by default projects = ['.'] # start it in the local project by default
all_projects = self.GetProjects(projects, all_projects = self.GetProjects(projects,
missing_ok=bool(self.gitc_manifest)) missing_ok=bool(self.gitc_manifest))
@ -113,7 +114,7 @@ revision specified in the manifest.
branch_merge = self.manifest.default.revisionExpr branch_merge = self.manifest.default.revisionExpr
if not project.StartBranch( if not project.StartBranch(
nb, branch_merge=branch_merge, revision=opt.revision): nb, branch_merge=branch_merge, revision=opt.revision):
err.append(project) err.append(project)
pm.end() pm.end()

View File

@ -31,6 +31,7 @@ import os
from color import Coloring from color import Coloring
import platform_utils import platform_utils
class Status(PagedCommand): class Status(PagedCommand):
common = True common = True
helpSummary = "Show the working tree status" helpSummary = "Show the working tree status"
@ -126,8 +127,8 @@ the following meanings:
continue continue
if item in proj_dirs_parents: if item in proj_dirs_parents:
self._FindOrphans(glob.glob('%s/.*' % item) + self._FindOrphans(glob.glob('%s/.*' % item) +
glob.glob('%s/*' % item), glob.glob('%s/*' % item),
proj_dirs, proj_dirs_parents, outstring) proj_dirs, proj_dirs_parents, outstring)
continue continue
outstring.append(''.join([status_header, item, '/'])) outstring.append(''.join([status_header, item, '/']))
@ -170,8 +171,8 @@ the following meanings:
class StatusColoring(Coloring): class StatusColoring(Coloring):
def __init__(self, config): def __init__(self, config):
Coloring.__init__(self, config, 'status') Coloring.__init__(self, config, 'status')
self.project = self.printer('header', attr = 'bold') self.project = self.printer('header', attr='bold')
self.untracked = self.printer('untracked', fg = 'red') self.untracked = self.printer('untracked', fg='red')
orig_path = os.getcwd() orig_path = os.getcwd()
try: try:
@ -179,8 +180,8 @@ the following meanings:
outstring = [] outstring = []
self._FindOrphans(glob.glob('.*') + self._FindOrphans(glob.glob('.*') +
glob.glob('*'), glob.glob('*'),
proj_dirs, proj_dirs_parents, outstring) proj_dirs, proj_dirs_parents, outstring)
if outstring: if outstring:
output = StatusColoring(self.manifest.globalConfig) output = StatusColoring(self.manifest.globalConfig)

View File

@ -53,6 +53,7 @@ except ImportError:
try: try:
import resource import resource
def _rlimit_nofile(): def _rlimit_nofile():
return resource.getrlimit(resource.RLIMIT_NOFILE) return resource.getrlimit(resource.RLIMIT_NOFILE)
except ImportError: except ImportError:
@ -81,13 +82,16 @@ from manifest_xml import GitcManifest
_ONE_DAY_S = 24 * 60 * 60 _ONE_DAY_S = 24 * 60 * 60
class _FetchError(Exception): class _FetchError(Exception):
"""Internal error thrown in _FetchHelper() when we don't want stack trace.""" """Internal error thrown in _FetchHelper() when we don't want stack trace."""
pass pass
class _CheckoutError(Exception): class _CheckoutError(Exception):
"""Internal error thrown in _CheckoutOne() when we don't want stack trace.""" """Internal error thrown in _CheckoutOne() when we don't want stack trace."""
class Sync(Command, MirrorSafeCommand): class Sync(Command, MirrorSafeCommand):
jobs = 1 jobs = 1
common = True common = True
@ -217,7 +221,7 @@ later is required to fix a server side protocol bug.
p.add_option('-l', '--local-only', p.add_option('-l', '--local-only',
dest='local_only', action='store_true', dest='local_only', action='store_true',
help="only update working tree, don't fetch") help="only update working tree, don't fetch")
p.add_option('--no-manifest-update','--nmu', p.add_option('--no-manifest-update', '--nmu',
dest='mp_update', action='store_false', default='true', dest='mp_update', action='store_false', default='true',
help='use the existing manifest checkout as-is. ' help='use the existing manifest checkout as-is. '
'(do not update to the latest revision)') '(do not update to the latest revision)')
@ -327,14 +331,14 @@ later is required to fix a server side protocol bug.
try: try:
try: try:
success = project.Sync_NetworkHalf( success = project.Sync_NetworkHalf(
quiet=opt.quiet, quiet=opt.quiet,
current_branch_only=opt.current_branch_only, current_branch_only=opt.current_branch_only,
force_sync=opt.force_sync, force_sync=opt.force_sync,
clone_bundle=not opt.no_clone_bundle, clone_bundle=not opt.no_clone_bundle,
no_tags=opt.no_tags, archive=self.manifest.IsArchive, no_tags=opt.no_tags, archive=self.manifest.IsArchive,
optimized_fetch=opt.optimized_fetch, optimized_fetch=opt.optimized_fetch,
prune=opt.prune, prune=opt.prune,
clone_filter=clone_filter) clone_filter=clone_filter)
self._fetch_times.Set(project, time.time() - start) self._fetch_times.Set(project, time.time() - start)
# Lock around all the rest of the code, since printing, updating a set # Lock around all the rest of the code, since printing, updating a set
@ -355,8 +359,8 @@ later is required to fix a server side protocol bug.
except _FetchError: except _FetchError:
pass pass
except Exception as e: except Exception as e:
print('error: Cannot fetch %s (%s: %s)' \ print('error: Cannot fetch %s (%s: %s)'
% (project.name, type(e).__name__, str(e)), file=sys.stderr) % (project.name, type(e).__name__, str(e)), file=sys.stderr)
err_event.set() err_event.set()
raise raise
finally: finally:
@ -396,8 +400,8 @@ later is required to fix a server side protocol bug.
err_event=err_event, err_event=err_event,
clone_filter=self.manifest.CloneFilter) clone_filter=self.manifest.CloneFilter)
if self.jobs > 1: if self.jobs > 1:
t = _threading.Thread(target = self._FetchProjectList, t = _threading.Thread(target=self._FetchProjectList,
kwargs = kwargs) kwargs=kwargs)
# Ensure that Ctrl-C will not freeze the repo process. # Ensure that Ctrl-C will not freeze the repo process.
t.daemon = True t.daemon = True
threads.add(t) threads.add(t)
@ -560,13 +564,23 @@ later is required to fix a server side protocol bug.
def _GCProjects(self, projects, opt, err_event): def _GCProjects(self, projects, opt, err_event):
gc_gitdirs = {} gc_gitdirs = {}
for project in projects: for project in projects:
# Make sure pruning never kicks in with shared projects.
if len(project.manifest.GetProjectsWithName(project.name)) > 1: if len(project.manifest.GetProjectsWithName(project.name)) > 1:
print('Shared project %s found, disabling pruning.' % project.name) print('%s: Shared project %s found, disabling pruning.' %
project.bare_git.config('--replace-all', 'gc.pruneExpire', 'never') (project.relpath, project.name))
if git_require((2, 7, 0)):
project.config.SetString('core.repositoryFormatVersion', '1')
project.config.SetString('extensions.preciousObjects', 'true')
else:
# This isn't perfect, but it's the best we can do with old git.
print('%s: WARNING: shared projects are unreliable when using old '
'versions of git; please upgrade to git-2.7.0+.'
% (project.relpath,),
file=sys.stderr)
project.config.SetString('gc.pruneExpire', 'never')
gc_gitdirs[project.gitdir] = project.bare_git gc_gitdirs[project.gitdir] = project.bare_git
has_dash_c = git_require((1, 7, 2)) if multiprocessing:
if multiprocessing and has_dash_c:
cpu_count = multiprocessing.cpu_count() cpu_count = multiprocessing.cpu_count()
else: else:
cpu_count = 1 cpu_count = 1
@ -588,7 +602,7 @@ later is required to fix a server side protocol bug.
bare_git.gc('--auto', config=config) bare_git.gc('--auto', config=config)
except GitError: except GitError:
err_event.set() err_event.set()
except: except Exception:
err_event.set() err_event.set()
raise raise
finally: finally:
@ -693,16 +707,16 @@ later is required to fix a server side protocol bug.
gitdir = os.path.join(self.manifest.topdir, path, '.git') gitdir = os.path.join(self.manifest.topdir, path, '.git')
if os.path.exists(gitdir): if os.path.exists(gitdir):
project = Project( project = Project(
manifest = self.manifest, manifest=self.manifest,
name = path, name=path,
remote = RemoteSpec('origin'), remote=RemoteSpec('origin'),
gitdir = gitdir, gitdir=gitdir,
objdir = gitdir, objdir=gitdir,
worktree = os.path.join(self.manifest.topdir, path), worktree=os.path.join(self.manifest.topdir, path),
relpath = path, relpath=path,
revisionExpr = 'HEAD', revisionExpr='HEAD',
revisionId = None, revisionId=None,
groups = None) groups=None)
if project.IsDirty() and opt.force_remove_dirty: if project.IsDirty() and opt.force_remove_dirty:
print('WARNING: Removing dirty project "%s": uncommitted changes ' print('WARNING: Removing dirty project "%s": uncommitted changes '
@ -733,7 +747,7 @@ later is required to fix a server side protocol bug.
if not opt.quiet: if not opt.quiet:
print('Using manifest server %s' % manifest_server) print('Using manifest server %s' % manifest_server)
if not '@' in manifest_server: if '@' not in manifest_server:
username = None username = None
password = None password = None
if opt.manifest_server_username and opt.manifest_server_password: if opt.manifest_server_username and opt.manifest_server_password:
@ -874,7 +888,7 @@ later is required to fix a server side protocol bug.
manifest_name = opt.manifest_name manifest_name = opt.manifest_name
smart_sync_manifest_path = os.path.join( smart_sync_manifest_path = os.path.join(
self.manifest.manifestProject.worktree, 'smart_sync_override.xml') self.manifest.manifestProject.worktree, 'smart_sync_override.xml')
if opt.smart_sync or opt.smart_tag: if opt.smart_sync or opt.smart_tag:
manifest_name = self._SmartSyncSetup(opt, smart_sync_manifest_path) manifest_name = self._SmartSyncSetup(opt, smart_sync_manifest_path)
@ -1033,6 +1047,10 @@ later is required to fix a server side protocol bug.
file=sys.stderr) file=sys.stderr)
sys.exit(1) sys.exit(1)
if not opt.quiet:
print('repo sync has finished successfully.')
def _PostRepoUpgrade(manifest, quiet=False): def _PostRepoUpgrade(manifest, quiet=False):
wrapper = Wrapper() wrapper = Wrapper()
if wrapper.NeedSetupGnuPG(): if wrapper.NeedSetupGnuPG():
@ -1041,6 +1059,7 @@ def _PostRepoUpgrade(manifest, quiet=False):
if project.Exists: if project.Exists:
project.PostRepoUpgrade() project.PostRepoUpgrade()
def _PostRepoFetch(rp, no_repo_verify=False, verbose=False): def _PostRepoFetch(rp, no_repo_verify=False, verbose=False):
if rp.HasChanges: if rp.HasChanges:
print('info: A new version of repo is available', file=sys.stderr) print('info: A new version of repo is available', file=sys.stderr)
@ -1059,6 +1078,7 @@ def _PostRepoFetch(rp, no_repo_verify=False, verbose=False):
print('repo version %s is current' % rp.work_git.describe(HEAD), print('repo version %s is current' % rp.work_git.describe(HEAD),
file=sys.stderr) file=sys.stderr)
def _VerifyTag(project): def _VerifyTag(project):
gpg_dir = os.path.expanduser('~/.repoconfig/gnupg') gpg_dir = os.path.expanduser('~/.repoconfig/gnupg')
if not os.path.exists(gpg_dir): if not os.path.exists(gpg_dir):
@ -1089,9 +1109,9 @@ def _VerifyTag(project):
cmd = [GIT, 'tag', '-v', cur] cmd = [GIT, 'tag', '-v', cur]
proc = subprocess.Popen(cmd, proc = subprocess.Popen(cmd,
stdout = subprocess.PIPE, stdout=subprocess.PIPE,
stderr = subprocess.PIPE, stderr=subprocess.PIPE,
env = env) env=env)
out = proc.stdout.read() out = proc.stdout.read()
proc.stdout.close() proc.stdout.close()
@ -1125,7 +1145,7 @@ class _FetchTimes(object):
old = self._times.get(name, t) old = self._times.get(name, t)
self._seen.add(name) self._seen.add(name)
a = self._ALPHA a = self._ALPHA
self._times[name] = (a*t) + ((1-a) * old) self._times[name] = (a * t) + ((1 - a) * old)
def _Load(self): def _Load(self):
if self._times is None: if self._times is None:
@ -1163,6 +1183,8 @@ class _FetchTimes(object):
# and supporting persistent-http[s]. It cannot change hosts from # and supporting persistent-http[s]. It cannot change hosts from
# request to request like the normal transport, the real url # request to request like the normal transport, the real url
# is passed during initialization. # is passed during initialization.
class PersistentTransport(xmlrpc.client.Transport): class PersistentTransport(xmlrpc.client.Transport):
def __init__(self, orig_host): def __init__(self, orig_host):
self.orig_host = orig_host self.orig_host = orig_host
@ -1197,7 +1219,7 @@ class PersistentTransport(xmlrpc.client.Transport):
if proxy: if proxy:
proxyhandler = urllib.request.ProxyHandler({ proxyhandler = urllib.request.ProxyHandler({
"http": proxy, "http": proxy,
"https": proxy }) "https": proxy})
opener = urllib.request.build_opener( opener = urllib.request.build_opener(
urllib.request.HTTPCookieProcessor(cookiejar), urllib.request.HTTPCookieProcessor(cookiejar),
@ -1254,4 +1276,3 @@ class PersistentTransport(xmlrpc.client.Transport):
def close(self): def close(self):
pass pass

View File

@ -33,6 +33,7 @@ else:
UNUSUAL_COMMIT_THRESHOLD = 5 UNUSUAL_COMMIT_THRESHOLD = 5
def _ConfirmManyUploads(multiple_branches=False): def _ConfirmManyUploads(multiple_branches=False):
if multiple_branches: if multiple_branches:
print('ATTENTION: One or more branches has an unusually high number ' print('ATTENTION: One or more branches has an unusually high number '
@ -44,17 +45,20 @@ def _ConfirmManyUploads(multiple_branches=False):
answer = input("If you are sure you intend to do this, type 'yes': ").strip() answer = input("If you are sure you intend to do this, type 'yes': ").strip()
return answer == "yes" return answer == "yes"
def _die(fmt, *args): def _die(fmt, *args):
msg = fmt % args msg = fmt % args
print('error: %s' % msg, file=sys.stderr) print('error: %s' % msg, file=sys.stderr)
sys.exit(1) sys.exit(1)
def _SplitEmails(values): def _SplitEmails(values):
result = [] result = []
for value in values: for value in values:
result.extend([s.strip() for s in value.split(',')]) result.extend([s.strip() for s in value.split(',')])
return result return result
class Upload(InteractiveCommand): class Upload(InteractiveCommand):
common = True common = True
helpSummary = "Upload changes for code review" helpSummary = "Upload changes for code review"
@ -137,13 +141,13 @@ Gerrit Code Review: https://www.gerritcodereview.com/
dest='auto_topic', action='store_true', dest='auto_topic', action='store_true',
help='Send local branch name to Gerrit Code Review') help='Send local branch name to Gerrit Code Review')
p.add_option('--re', '--reviewers', p.add_option('--re', '--reviewers',
type='string', action='append', dest='reviewers', type='string', action='append', dest='reviewers',
help='Request reviews from these people.') help='Request reviews from these people.')
p.add_option('--cc', p.add_option('--cc',
type='string', action='append', dest='cc', type='string', action='append', dest='cc',
help='Also send email to these email addresses.') help='Also send email to these email addresses.')
p.add_option('--br', p.add_option('--br',
type='string', action='store', dest='branch', type='string', action='store', dest='branch',
help='Branch to upload.') help='Branch to upload.')
p.add_option('--cbr', '--current-branch', p.add_option('--cbr', '--current-branch',
dest='current_branch', action='store_true', dest='current_branch', action='store_true',
@ -168,6 +172,9 @@ Gerrit Code Review: https://www.gerritcodereview.com/
type='string', action='store', dest='dest_branch', type='string', action='store', dest='dest_branch',
metavar='BRANCH', metavar='BRANCH',
help='Submit for review on this target branch.') help='Submit for review on this target branch.')
p.add_option('--no-cert-checks',
dest='validate_certs', action='store_false', default=True,
help='Disable verifying ssl certs (unsafe).')
# Options relating to upload hook. Note that verify and no-verify are NOT # Options relating to upload hook. Note that verify and no-verify are NOT
# opposites of each other, which is why they store to different locations. # opposites of each other, which is why they store to different locations.
@ -185,15 +192,16 @@ Gerrit Code Review: https://www.gerritcodereview.com/
# Never run upload hooks, but upload anyway (AKA bypass hooks). # Never run upload hooks, but upload anyway (AKA bypass hooks).
# - no-verify=True, verify=True: # - no-verify=True, verify=True:
# Invalid # Invalid
p.add_option('--no-cert-checks', g = p.add_option_group('Upload hooks')
dest='validate_certs', action='store_false', default=True, g.add_option('--no-verify',
help='Disable verifying ssl certs (unsafe).')
p.add_option('--no-verify',
dest='bypass_hooks', action='store_true', dest='bypass_hooks', action='store_true',
help='Do not run the upload hook.') help='Do not run the upload hook.')
p.add_option('--verify', g.add_option('--verify',
dest='allow_all_hooks', action='store_true', dest='allow_all_hooks', action='store_true',
help='Run the upload hook without prompting.') help='Run the upload hook without prompting.')
g.add_option('--ignore-hooks',
dest='ignore_hooks', action='store_true',
help='Do not abort uploading if upload hooks fail.')
def _SingleBranch(self, opt, branch, people): def _SingleBranch(self, opt, branch, people):
project = branch.project project = branch.project
@ -214,10 +222,10 @@ Gerrit Code Review: https://www.gerritcodereview.com/
print('Upload project %s/ to remote branch %s%s:' % print('Upload project %s/ to remote branch %s%s:' %
(project.relpath, destination, ' (draft)' if opt.draft else '')) (project.relpath, destination, ' (draft)' if opt.draft else ''))
print(' branch %s (%2d commit%s, %s):' % ( print(' branch %s (%2d commit%s, %s):' % (
name, name,
len(commit_list), len(commit_list),
len(commit_list) != 1 and 's' or '', len(commit_list) != 1 and 's' or '',
date)) date))
for commit in commit_list: for commit in commit_list:
print(' %s' % commit) print(' %s' % commit)
@ -322,12 +330,12 @@ Gerrit Code Review: https://www.gerritcodereview.com/
key = 'review.%s.autoreviewer' % project.GetBranch(name).remote.review key = 'review.%s.autoreviewer' % project.GetBranch(name).remote.review
raw_list = project.config.GetString(key) raw_list = project.config.GetString(key)
if not raw_list is None: if raw_list is not None:
people[0].extend([entry.strip() for entry in raw_list.split(',')]) people[0].extend([entry.strip() for entry in raw_list.split(',')])
key = 'review.%s.autocopy' % project.GetBranch(name).remote.review key = 'review.%s.autocopy' % project.GetBranch(name).remote.review
raw_list = project.config.GetString(key) raw_list = project.config.GetString(key)
if not raw_list is None and len(people[0]) > 0: if raw_list is not None and len(people[0]) > 0:
people[1].extend([entry.strip() for entry in raw_list.split(',')]) people[1].extend([entry.strip() for entry in raw_list.split(',')])
def _FindGerritChange(self, branch): def _FindGerritChange(self, branch):
@ -418,18 +426,18 @@ Gerrit Code Review: https://www.gerritcodereview.com/
else: else:
fmt = '\n (%s)' fmt = '\n (%s)'
print(('[FAILED] %-15s %-15s' + fmt) % ( print(('[FAILED] %-15s %-15s' + fmt) % (
branch.project.relpath + '/', \ branch.project.relpath + '/',
branch.name, \ branch.name,
str(branch.error)), str(branch.error)),
file=sys.stderr) file=sys.stderr)
print() print()
for branch in todo: for branch in todo:
if branch.uploaded: if branch.uploaded:
print('[OK ] %-15s %s' % ( print('[OK ] %-15s %s' % (
branch.project.relpath + '/', branch.project.relpath + '/',
branch.name), branch.name),
file=sys.stderr) file=sys.stderr)
if have_errors: if have_errors:
sys.exit(1) sys.exit(1)
@ -437,14 +445,14 @@ Gerrit Code Review: https://www.gerritcodereview.com/
def _GetMergeBranch(self, project): def _GetMergeBranch(self, project):
p = GitCommand(project, p = GitCommand(project,
['rev-parse', '--abbrev-ref', 'HEAD'], ['rev-parse', '--abbrev-ref', 'HEAD'],
capture_stdout = True, capture_stdout=True,
capture_stderr = True) capture_stderr=True)
p.Wait() p.Wait()
local_branch = p.stdout.strip() local_branch = p.stdout.strip()
p = GitCommand(project, p = GitCommand(project,
['config', '--get', 'branch.%s.merge' % local_branch], ['config', '--get', 'branch.%s.merge' % local_branch],
capture_stdout = True, capture_stdout=True,
capture_stderr = True) capture_stderr=True)
p.Wait() p.Wait()
merge_branch = p.stdout.strip() merge_branch = p.stdout.strip()
return merge_branch return merge_branch
@ -488,12 +496,24 @@ Gerrit Code Review: https://www.gerritcodereview.com/
abort_if_user_denies=True) abort_if_user_denies=True)
pending_proj_names = [project.name for (project, available) in pending] pending_proj_names = [project.name for (project, available) in pending]
pending_worktrees = [project.worktree for (project, available) in pending] pending_worktrees = [project.worktree for (project, available) in pending]
passed = True
try: try:
hook.Run(opt.allow_all_hooks, project_list=pending_proj_names, hook.Run(opt.allow_all_hooks, project_list=pending_proj_names,
worktree_list=pending_worktrees) worktree_list=pending_worktrees)
except SystemExit:
passed = False
if not opt.ignore_hooks:
raise
except HookError as e: except HookError as e:
passed = False
print("ERROR: %s" % str(e), file=sys.stderr) print("ERROR: %s" % str(e), file=sys.stderr)
return
if not passed:
if opt.ignore_hooks:
print('\nWARNING: pre-upload hooks failed, but uploading anyways.',
file=sys.stderr)
else:
return
if opt.reviewers: if opt.reviewers:
reviewers = _SplitEmails(opt.reviewers) reviewers = _SplitEmails(opt.reviewers)

View File

@ -20,6 +20,7 @@ from command import Command, MirrorSafeCommand
from git_command import git, RepoSourceVersion, user_agent from git_command import git, RepoSourceVersion, user_agent
from git_refs import HEAD from git_refs import HEAD
class Version(Command, MirrorSafeCommand): class Version(Command, MirrorSafeCommand):
wrapper_version = None wrapper_version = None
wrapper_path = None wrapper_path = None

View File

@ -23,14 +23,17 @@ import unittest
import git_config import git_config
def fixture(*paths): def fixture(*paths):
"""Return a path relative to test/fixtures. """Return a path relative to test/fixtures.
""" """
return os.path.join(os.path.dirname(__file__), 'fixtures', *paths) return os.path.join(os.path.dirname(__file__), 'fixtures', *paths)
class GitConfigUnitTest(unittest.TestCase): class GitConfigUnitTest(unittest.TestCase):
"""Tests the GitConfig class. """Tests the GitConfig class.
""" """
def setUp(self): def setUp(self):
"""Create a GitConfig object using the test.gitconfig fixture. """Create a GitConfig object using the test.gitconfig fixture.
""" """
@ -68,5 +71,6 @@ class GitConfigUnitTest(unittest.TestCase):
val = config.GetString('empty') val = config.GetString('empty')
self.assertEqual(val, None) self.assertEqual(val, None)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -49,6 +49,8 @@ class ManifestValidateFilePaths(unittest.TestCase):
# We allow symlinks to end in a slash since we allow them to point to dirs # We allow symlinks to end in a slash since we allow them to point to dirs
# in general. Technically the slash isn't necessary. # in general. Technically the slash isn't necessary.
check('foo/', 'bar') check('foo/', 'bar')
# We allow a single '.' to get a reference to the project itself.
check('.', 'bar')
def test_bad_paths(self): def test_bad_paths(self):
"""Make sure bad paths (src & dest) are rejected.""" """Make sure bad paths (src & dest) are rejected."""

View File

@ -163,7 +163,7 @@ class CopyLinkTestCase(unittest.TestCase):
@staticmethod @staticmethod
def touch(path): def touch(path):
with open(path, 'w') as f: with open(path, 'w'):
pass pass
def assertExists(self, path, msg=None): def assertExists(self, path, msg=None):
@ -256,7 +256,7 @@ class CopyFile(CopyLinkTestCase):
cf = self.CopyFile('bar/foo.txt', 'foo') cf = self.CopyFile('bar/foo.txt', 'foo')
self.assertRaises(error.ManifestInvalidPathError, cf._Copy) self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
def test_src_block_dir(self): def test_src_block_copy_from_dir(self):
"""Do not allow copying from a directory.""" """Do not allow copying from a directory."""
src = os.path.join(self.worktree, 'dir') src = os.path.join(self.worktree, 'dir')
os.makedirs(src) os.makedirs(src)
@ -279,7 +279,7 @@ class CopyFile(CopyLinkTestCase):
cf = self.CopyFile('foo.txt', 'sym/foo.txt') cf = self.CopyFile('foo.txt', 'sym/foo.txt')
self.assertRaises(error.ManifestInvalidPathError, cf._Copy) self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
def test_src_block_dir(self): def test_src_block_copy_to_dir(self):
"""Do not allow copying to a directory.""" """Do not allow copying to a directory."""
src = os.path.join(self.worktree, 'foo.txt') src = os.path.join(self.worktree, 'foo.txt')
self.touch(src) self.touch(src)
@ -314,6 +314,14 @@ class LinkFile(CopyLinkTestCase):
lf._Link() lf._Link()
self.assertExists(os.path.join(self.topdir, 'foo')) self.assertExists(os.path.join(self.topdir, 'foo'))
def test_src_self(self):
"""Link to the project itself."""
dest = os.path.join(self.topdir, 'foo', 'bar')
lf = self.LinkFile('.', 'foo/bar')
lf._Link()
self.assertExists(dest)
self.assertEqual('../git-project', os.readlink(dest))
def test_dest_subdir(self): def test_dest_subdir(self):
"""Link a file to a subdir of a checkout.""" """Link a file to a subdir of a checkout."""
src = os.path.join(self.worktree, 'foo.txt') src = os.path.join(self.worktree, 'foo.txt')
@ -323,6 +331,21 @@ class LinkFile(CopyLinkTestCase):
lf._Link() lf._Link()
self.assertExists(os.path.join(self.topdir, 'sub', 'dir', 'foo', 'bar')) self.assertExists(os.path.join(self.topdir, 'sub', 'dir', 'foo', 'bar'))
def test_src_block_relative(self):
"""Do not allow relative symlinks."""
BAD_SOURCES = (
'./',
'..',
'../',
'foo/.',
'foo/./bar',
'foo/..',
'foo/../foo',
)
for src in BAD_SOURCES:
lf = self.LinkFile(src, 'foo')
self.assertRaises(error.ManifestInvalidPathError, lf._Link)
def test_update(self): def test_update(self):
"""Make sure changed targets get updated.""" """Make sure changed targets get updated."""
dest = os.path.join(self.topdir, 'sym') dest = os.path.join(self.topdir, 'sym')

View File

@ -19,24 +19,53 @@
from __future__ import print_function from __future__ import print_function
import os import os
import re
import unittest import unittest
from pyversion import is_python3
import wrapper import wrapper
if is_python3():
from unittest import mock
from io import StringIO
else:
import mock
from StringIO import StringIO
def fixture(*paths): def fixture(*paths):
"""Return a path relative to tests/fixtures. """Return a path relative to tests/fixtures.
""" """
return os.path.join(os.path.dirname(__file__), 'fixtures', *paths) return os.path.join(os.path.dirname(__file__), 'fixtures', *paths)
class RepoWrapperUnitTest(unittest.TestCase):
"""Tests helper functions in the repo wrapper class RepoWrapperTestCase(unittest.TestCase):
""" """TestCase for the wrapper module."""
def setUp(self): def setUp(self):
"""Load the wrapper module every time """Load the wrapper module every time."""
"""
wrapper._wrapper_module = None wrapper._wrapper_module = None
self.wrapper = wrapper.Wrapper() self.wrapper = wrapper.Wrapper()
if not is_python3():
self.assertRegex = self.assertRegexpMatches
class RepoWrapperUnitTest(RepoWrapperTestCase):
"""Tests helper functions in the repo wrapper
"""
def test_version(self):
"""Make sure _Version works."""
with self.assertRaises(SystemExit) as e:
with mock.patch('sys.stdout', new_callable=StringIO) as stdout:
with mock.patch('sys.stderr', new_callable=StringIO) as stderr:
self.wrapper._Version()
self.assertEqual(0, e.exception.code)
self.assertEqual('', stderr.getvalue())
self.assertIn('repo launcher version', stdout.getvalue())
def test_get_gitc_manifest_dir_no_gitc(self): def test_get_gitc_manifest_dir_no_gitc(self):
""" """
Test reading a missing gitc config file Test reading a missing gitc config file
@ -76,5 +105,38 @@ class RepoWrapperUnitTest(unittest.TestCase):
self.assertEqual(self.wrapper.gitc_parse_clientdir('/gitc/manifest-rw/'), None) self.assertEqual(self.wrapper.gitc_parse_clientdir('/gitc/manifest-rw/'), None)
self.assertEqual(self.wrapper.gitc_parse_clientdir('/test/usr/local/google/gitc/'), None) self.assertEqual(self.wrapper.gitc_parse_clientdir('/test/usr/local/google/gitc/'), None)
class SetGitTrace2ParentSid(RepoWrapperTestCase):
"""Check SetGitTrace2ParentSid behavior."""
KEY = 'GIT_TRACE2_PARENT_SID'
VALID_FORMAT = re.compile(r'^repo-[0-9]{8}T[0-9]{6}Z-P[0-9a-f]{8}$')
def test_first_set(self):
"""Test env var not yet set."""
env = {}
self.wrapper.SetGitTrace2ParentSid(env)
self.assertIn(self.KEY, env)
value = env[self.KEY]
self.assertRegex(value, self.VALID_FORMAT)
def test_append(self):
"""Test env var is appended."""
env = {self.KEY: 'pfx'}
self.wrapper.SetGitTrace2ParentSid(env)
self.assertIn(self.KEY, env)
value = env[self.KEY]
self.assertTrue(value.startswith('pfx/'))
self.assertRegex(value[4:], self.VALID_FORMAT)
def test_global_context(self):
"""Check os.environ gets updated by default."""
os.environ.pop(self.KEY, None)
self.wrapper.SetGitTrace2ParentSid()
self.assertIn(self.KEY, os.environ)
value = os.environ[self.KEY]
self.assertRegex(value, self.VALID_FORMAT)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -20,3 +20,8 @@ envlist = py27, py36, py37, py38
[testenv] [testenv]
deps = pytest deps = pytest
commands = {toxinidir}/run_tests commands = {toxinidir}/run_tests
[testenv:py27]
deps =
mock
pytest

View File

@ -27,7 +27,10 @@ import os
def WrapperPath(): def WrapperPath():
return os.path.join(os.path.dirname(__file__), 'repo') return os.path.join(os.path.dirname(__file__), 'repo')
_wrapper_module = None _wrapper_module = None
def Wrapper(): def Wrapper():
global _wrapper_module global _wrapper_module
if not _wrapper_module: if not _wrapper_module: