Compare commits

..

146 Commits

Author SHA1 Message Date
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
83a3227b62 Fixing forall subcommand for Py3
Execution of 'repo forall -p -c' doesn't work with Py3 and ends up
with an error:

Got an error, terminating the pool: TypeError: can only concatenate
str (not "bytes") to str

That's fixed by using the decode() method.

Change-Id: Ice01aaa1822dde8d957b5bf096021dd5a2b7dd51
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/253659
Reviewed-by: Mike Frysinger <vapier@google.com>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Jiri Tyr <jiri.tyr@gmail.com>
2020-02-10 10:52:27 +00:00
09dd9bda38 docs: document internal manifests.git/config settings
Change-Id: I6b32d925756375a9335522ff33376cb5f7ed1157
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254073
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
2020-02-10 00:12:17 +00:00
f914edca53 project: unify HEAD path management
Add a helper function to unify the duplication of finding the full
path to the symbolic HEAD ref.  This makes it easy to handle git
worktrees where .git is a file rather than a dir/symlink.

Bug: https://crbug.com/gerrit/11486
Change-Id: I9f794f1295ad0d98c7c13622f01ded51e4ba7846
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254074
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
2020-02-09 23:26:05 +00:00
e7c91889a6 remove spurious +x bits
These files are not directly executable, so drop the +x bits.

Change-Id: Iaf19a03a497686cc21103e7ddf08073173440dd1
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254076
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
2020-02-09 23:24:03 +00:00
1b117db767 find python via env
This allows these scripts to run through the active version of the
virtualenv python when invoked via tox.

Change-Id: Ib52f475b7b20c34d62cfd179a1341da1a08a8b5c
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/253974
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2020-02-09 04:02:45 +00:00
563f1a6512 repo: allow REPO_REV to be an env var
We do this for REPO_URL already.

Bug: https://crbug.com/gerrit/10233
Change-Id: I53410645474b00d900467c96fa5d8446f3a607d3
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/253552
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2020-02-09 04:02:26 +00:00
b4687ad862 docs: add a developer reference for .repo/ paths
Currently the only reference for these is the source which can be a
pita when needing to refer to something quickly.

Change-Id: I52baeb9a4935814cf99fa9a9b3102e8e46cddb0d
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/253972
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
2020-02-09 03:55:47 +00:00
ded477dbb9 git_config: fix encoding handling in GetUrlCookieFile
Make sure we decode the bytes coming from the subprocess.Popen as
we're treating them as strings.

Change-Id: I44100ca5cd94f68a35d489936292eb641006edbe
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/253973
Reviewed-by: Jonathan Nieder <jrn@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
2020-02-08 06:26:36 +00:00
93293ca47f Fix inverted logic around [gitc-]init and -c
Instead of not using '-c' for '--current-branch' when using gitc, we
were only using '-c' when using gitc, so we still had the conflict with
the gitc option, and other users still couldn't use '-c'.

Test: repo init -u https://android.googlesource.com/platform/manifest; repo init -c
Test: repo gitc-init -u ... -b ... -c testing
Change-Id: I71e4950a49c281418249f0783c6a2ea34f0d3e2b
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/253795
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Dan Willemsen <dwillemsen@google.com>
2020-02-07 20:54:34 +00:00
dbd277ce50 [Win32] Make platform_utils compatible for Python3
On Python 3 several imports are to be imported from
different locations.

Signed-off-by: Remy Böhmer <linux@bohmer.net>
Change-Id: I4f243d145f65e38f74743a742583cfc5c5d76deb
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/249610
Reviewed-by: Mike Frysinger <vapier@google.com>
2020-02-06 14:29:15 +00:00
5a03308c5c sync: try to checkout repos across sync failures
Currently our default behavior is:
* Try to sync all repos
  * If any errors seen, exit
* Try to garbage collect all repos
  * If any errors seen, exit
* Try to update local project list
  * If any errors seen, exit
* Try to checkout out all local repos
  * If any errors seen, exit

Users find these incomplete syncs confusing, so lets try to complete
as much as possible by default and printing out summaries at the end.

Bug: https://crbug.com/gerrit/11293
Change-Id: Idd17cc9c3bbc574d8a0f08a30225dec7bfe414cb
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/238554
Reviewed-by: Michael Mortensen <mmortensen@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
2020-02-05 21:37:20 +00:00
3ba716f382 repo: try to reexec self with Python 3 as needed
We want to start warning about Python 2 usage, but we can't do it
simply because the shebang is /usr/bin/python which might be an old
version like python2.7.

We can't change the shebang because program name usage is spotty at
best: on some platforms (like macOS), it's not uncommon to not have
a `python3` wrapper, only a major.minor one like `python3.6`.  Using
python3 wouldn't guarantee a new enough version of Python 3 anyways,
and we don't want to require Python 3.6 exactly, just that minimum.

So we check the current Python version.  If it's older than the ver
of Python 3 we want, we search for a `python3.X` version to run.  If
those don't work, we see if `python3` exists and is a new enough ver.
If it's not, we die if the current Python 3 is too old, and we start
issuing warnings if the current Python version is 2.7.  This should
allow the user to take a bit more action by installing Python 3 on
their system without having to worry about changing /usr/bin/python.

Once we require Python 3 completely, we can simplify this logic a bit
by always bootstrapping up to Python 3 and failing with Python 2.

We have a few KI with Windows atm though, so keep it disabled there
until the fixes are merged.

Bug: https://crbug.com/gerrit/10418
Change-Id: I5e157defc788e31efb3e21e93f53fabdc7d75a3c
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/253136
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
2020-02-05 21:32:02 +00:00
655aedd7f3 repo: raise min version of git
The git-2.10 series was released in 2016.  Since we're moving to
require Python 3.6 which was also released in 2016, bumping up the
git version seems reasonable.  Also we don't really test any git
versions close to as old as 1.7.2 which was released in 2010.

Change-Id: Ib71b714de6cd0b7dd50d0b300b108a560ee27331
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/253134
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
2020-02-05 19:01:40 +00:00
cc960971f4 sync: add option to skip manifest update
The use case is any situation where your manifest does
not exist on server, but where you still want to do
full sync for the projects, without having your
workspace manifest switched to other branch or
forwarded to latest or similar.
This allows syncing to a historical manifest in git log,
that does not have a branch, as well as when integrating
something together that has not been pushed upstream yet.
Changes can also exist locally on a manifest that is
behind head, meaning not requiring rebase to latest.

Tested using:
  $ cd .repo/manifests/
  $ git checkout <any hash 1>
  $ <do local modifications>
  $ repo sync --no-manifest-update
  $ git checkout <any hash 2>
  $ repo sync --no-manifest-update

Change-Id: I0c9773aa8bc5876813a2e7d7fec697abcb2d9e94
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/246445
Tested-by: Fredrik de Groot <fredrik.de.groot@volvocars.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
2020-02-05 18:57:58 +00:00
66098f707a init: handle -c conflicts with gitc-init
We keep getting requests for init to support -c.  This conflicts with
gitc-init which allocates -c for its own use.  Lets make this dynamic
so we keep it with "init" but omit it for "gitc-init".

Bug: https://crbug.com/gerrit/10200
Change-Id: Ibf69c2bbeff638e28e63cb08926fea0c622258db
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/253252
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2020-02-05 16:00:10 +00:00
f7b64e3350 Do not try to fetch default revision for mirrors always
* Mirrors may contain multiple projects, some of which may not
  always contain the default revision.
* Only fetch the default revision explicitly if
  '--current-branch' is set.
* Fixes breakage casued by
  commit 6856f98467
  "Fix repo mirror with --current-branch"

Bug: https://crbug.com/gerrit/12274
Change-Id: Iaafabe2992f76f3644b841f24245d3e19c9515a9
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/253093
Reviewed-by: Kuang-che Wu <kcwu@chromium.org>
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Chirayu Desai <chirayudesai1@gmail.com>
2020-02-05 15:51:18 +00:00
bd0aae95f5 Add a way to override the remote using <extend-project>
This commit supports for the 'remote' attribute in
<extend-project>. This avoids the need to perform a <remove-project>
followed by a <project> in local manifests.

Change-Id: I9f9347913337ec9d159bc264d15ce97881ae5398
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/253092
Tested-by: Kyunam Jo <kyunam.jo@gmail.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
2020-02-04 22:42:28 +00:00
e6a202f790 project: add basic path checks for <copyfile> & <linkfile>
Reject paths in <copyfile> & <linkfile> that try to use symlinks or
non-file or non-dirs.

We don't fully validate <linkfile> when src is a glob as it's a bit
complicated -- any component in the src could be the glob.  We make
sure the destination is a directory, and that any paths in that dir
are created as symlinks.  So while this can be used to read any path,
it can't be abused to write to any paths.

Bug: https://crbug.com/gerrit/11218
Change-Id: I68b6d789b5ca4e43f569e75e8b293b3e13d3224b
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/233074
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
Reviewed-by: Michael Mortensen <mmortensen@google.com>
2020-02-04 20:34:23 +00:00
04122b7261 manifest: add basic path checks for <copyfile> & <linkfile>
Reject paths in <copyfile> & <linkfile> that point outside of their
respective scopes.  This validates paths while parsing the manifest
as this should be quick & cheap: we don't access the filesystem as
this code runs before we've synced.

Bug: https://crbug.com/gerrit/11218
Change-Id: I8e17bb91f3f5b905a9d76391b29fbab4cb77aa58
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/232932
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
Reviewed-by: Michael Mortensen <mmortensen@google.com>
2020-02-04 20:34:01 +00:00
f5525fb310 repo: drop old signing key
This hasn't been used in many years to sign a release, so drop it
from the keyring to avoid confusing people.

Bug: https://crbug.com/gerrit/12229
Change-Id: Ifca7eee713d167c11f32252975724e5858e4c007
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/253133
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2020-02-04 06:39:42 +00:00
ee451f035d repo: bump launcher version to 2.0
This reflects the transition to the new 2.x series which will be
migrating to Python 3-only.

Bug: https://crbug.com/gerrit/10418
Change-Id: I6355ac955d26b930f8a3721d3526eec5bed92400
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/253132
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2020-02-04 06:15:26 +00:00
91d9587e45 Revert "Port _FileDescriptorStreamsNonBlocking to use poll()"
This reverts commit 1e01a74445.

Not all platforms support select.poll() currently it seems.
At least macOS's Python 2 doesn't (while macOS Python 3 does).

Lets back this out for the existing release series and once we
start repo-2 which is Python 3-only, we can put this back in.

Change-Id: I205206b0fa4fe2d755f4fbc6ec683ad125f27cc2
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/253072
Reviewed-by: Michael Mortensen <mmortensen@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
2020-02-03 23:01:07 +00:00
0bcc2d28d4 Fix docstring of project.Project.PrintWorkTreeStatus()
Change-Id: I1a9139d2ea3b3331a6f3ad3cae9e0ac37074d716
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/251837
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Rostislav Krasny <rostigm@gmail.com>
2020-01-25 13:29:56 +00:00
ec0ba2777f Fix method signature of platform_utils.FileDescriptorStreams._create_stream()
Change-Id: Ib80e4ec5e540d97488e7564703ddbcb74350fdfd
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/251836
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Rostislav Krasny <rostigm@gmail.com>
2020-01-25 13:29:08 +00:00
9da67feecf Fix a typo
Change-Id: I1d1d1c7ec6c0c706eb08ceb803c37e1ce1baf8b3
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/251834
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Rostislav Krasny <rostigm@gmail.com>
2020-01-25 00:53:39 +00:00
b0b164a87f Add PyCharm project directory into the .gitignore
Change-Id: I9a785a9d045e44c6ec8bd4bd8d0169a81d5ccfde
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/251835
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Rostislav Krasny <rostigm@gmail.com>
2020-01-24 23:09:49 +00:00
b71d61d34e Make _preserve_encoding in platform_utils_win32 compatible with Python 3
Bug: https://crbug.com/gerrit/12145
Change-Id: I01d1ef96ff7b474f55ed42ecc13bd5943006d3b5
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/251833
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Rostislav Krasny <rostigm@gmail.com>
2020-01-24 21:50:22 +00:00
8f997b38cb repo: Do not even try to set up GPG with opt.no_repo_verify
In order to be able to use "--no-repo-verify" to work around an issue with
gpg-agent and long socket paths (see e.g. [1]), this change avoids GPG
being set up at all if that option is passed.

[1] https://github.com/elastic/elasticsearch/issues/17053

Change-Id: I1e5cbd8be2dc0084f12afe0ca33c789fdbc6fef9
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/251108
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
2020-01-24 16:10:51 +00:00
0eb2d3c8a0 init: Add '-c' as an alias to '--current-branch'
This makes it consistent with the short option for current-branch in
repo sync.

Change-Id: I2848e87f45a66ef8d829576d0c0c4c0f7a8636a0
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/241700
Tested-by: Diogo Ferreira <deovferreira@gmail.com>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Reviewed-by: Mike Frysinger <vapier@google.com>
2020-01-24 14:46:58 +00:00
e4d20372b2 info: Add the manifest revision
After Ib546f5ebbc8a23875fbd14bf166fbe95b7dd244e, repo info now displays
the current project revision in the 'Current revision' field.

While the output is more consistent, there are use cases for the
revision expression as shown in the manifest. This patch re-adds the
manifest revision as a new 'Manifest revision' field.

Change-Id: I50c1559dcb7ceb69af07352b956d78f85b8f592e
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/240799
Tested-by: Diogo Ferreira <deovferreira@gmail.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
2020-01-24 14:46:30 +00:00
1e01a74445 Port _FileDescriptorStreamsNonBlocking to use poll()
select() has a limit of FD_SETSIZE file descriptors. If you run repo
sync -j500 you'll pretty quickly hit this limit and get "file descriptor
out of range for select" errors. poll() has no such limit.

Change-Id: I21f350e472bda1db03dcbcc437645c23dbc7a901
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/248852
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Theodore Dubois <tbodt@google.com>
2019-12-18 21:16:23 +00:00
7c321f1bf6 repo: include subcommands in --help output
Also point people to `repo help` so it's easier to navigate the tool.

Bug: https://crbug.com/gerrit/12022
Change-Id: Ib3be331a2cef32caa193640bf8d54bd1443fce60
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/247292
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
2019-12-05 05:00:21 +00:00
7ac12a9b22 docs: add Windows support info
Change-Id: I82a1bec3a29d622c76b5709b96bbe8bff8aa427f
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/247573
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
2019-12-05 01:02:08 +00:00
0b304c06ff help: unify command display
No functional changes, just unifying duplicate code paths.

Change-Id: I6afa797ca1e1eb90abdc0236325003ae070cbfb3
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/247293
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-12-03 02:31:05 +00:00
4997d1c838 tox: add & document tox usage
This makes it easy to run all the tests against multiple versions
of Python.  We want to make sure Python 2.7 & 3.6+ work.

Change-Id: Ia7b16eb46a2aa7c240f03bb291987fa8cb215267
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/247174
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-12-02 04:24:23 +00:00
5b3a57c3ff setup.py: add basic packaging files
This is needed to use tox, and tox lets us test multiple Python
versions easily.

Change-Id: I813c418a8f7109294a4adb9f6b21be459cbeca70
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/247173
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-12-02 04:23:31 +00:00
6f8c85ce2a run_tests: improve exit code behavior
Rather than throw an exception when pytest itself exits non-zero,
pass that back up.  The traceback is never useful, only confusing.

Change-Id: I0cd7bea730e13c9969154326057196295e550843
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/247175
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-12-02 04:20:10 +00:00
6856f98467 Fix repo mirror with --current-branch
Before a2cd6aeae8, "repo mirror with --current-branch" fetches git data
using command
    git fetch --progress --update-head-ok cros --tags
No refspec is specified, thus it fetches default refspec, which is +refs/heads/*:refs/heads/*

After a2cd6aeae8, the fetch command became
     git fetch --progress --update-head-ok cros --tags +refs/tags/*:refs/tags/*
It did not only add tags refspec, but also suppressed the fetching of default refspec.

In other words, repo mirrors doesn't work if current_branch_only=True.
This CL explicitly adds the default refspec to command line if none is
specified.

Bug: https://crbug.com/gerrit/11990
Change-Id: Iadcf7b9aa50f53c47132cfe6c53b3fb2076ebca2
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/246632
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Kuang-che Wu <kcwu@chromium.org>
2019-11-25 20:12:34 +00:00
34bc5712eb README: add install details
Change-Id: I57043449a7927068fa5735cb71633353e1039532
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/245816
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-11-18 19:17:48 +00:00
70c54dc255 upload/editor: fix bytes/string confusion
The upload module tries to turn the strings into bytes before passing
to EditString, but it combines bytes & strings causing an error.  The
return value might be bytes or string, but the caller only expects a
string.  Lets simplify this by sticking to strings everywhere and have
EditString take care of converting to/from bytes when reading/writing
the underlying files.  This also avoids possible locale confusion when
reading the file by forcing UTF-8 everywhere.

Bug: https://crbug.com/gerrit/11929
Change-Id: I07b146170c5e8b5b0500a2c79e4213cd12140a96
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/245621
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-11-16 23:55:30 +00:00
6da17751ca prune: handle branches that track missing branches
Series of steps:
* Create a local "b1" branch with `repo start b1` that tracks a remote
  branch (totally fine)
* Manually create a local "b2" branch with `git branch --track b1 b2`
  that tracks the local "b1" (uh-oh...)
* Delete the local "b1" branch manually or via `repo prune` (....)
* Try to process the "b2" branch with `repo prune`

Since b2 tracks a branch that no longer exists, everything blows up
at this point as we try to probe the non-existent ref.  Instead, we
should flag this as unknown and leave it up to the user to resolve.

This probably could come up if a local branch was tracking a remote
branch that was deleted from the server, and users ran something like
`repo sync --prune` which cleaned up the remote refs.

Bug: https://crbug.com/gerrit/11485
Change-Id: I6b6b6041943944b8efa6e2ad0b8b10f13a75a5c2
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/236793
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Reviewed-by: Kirtika Ruchandani <kirtika@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
2019-11-16 19:55:02 +00:00
2ba5a1e963 sync: try to fast forward merge branches before checking published state
If the local branch changed state since its last upload, the data
cached in .git/config related to the last uploaded CL might not be
that relevant.  If we're able to fast forward merge to the latest
tree state, then let's do that.  This would be akin to checking
out a detached head before syncing where we already switch state.

If we aren't able to fast forward merge, then it's not a big deal
as we'll continue on to the existing branch checking logic.

This would be easy to reproduce by doing something like:
  $ repo start foo .
  $ git revert HEAD
  $ repo upload --cbr .
  $ git reset --hard HEAD^
  <CL is merged>
  $ repo sync .
  <we can fast forward>

Change-Id: I7d62f3d1ba5314a349d85b4dbb0ec8352eca18bb
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/238552
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: Michael Mortensen <mmortensen@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
2019-11-13 20:29:33 +00:00
3538dd224d sync: merge project updates with status bar
The current sync output displays "Fetching project" and "Checking out
project" messages and progress bar updates independently leading to a
lot of spam.  Lets merge these periodic outputs with the status bar to
get a little bit tighter output in the normal case.  This doesn't solve
all our problems, but gets us closer.

Bug: https://crbug.com/gerrit/11293
Change-Id: Icd627830af4dd934a9355b7ace754b56dc96cfef
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/244934
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-11-12 23:33:51 +00:00
b610b850ac sync: add sanity check for local checkouts missing network
If you run `repo sync -l foo` without first `repo sync -n foo`,
repo sets up an invalid gitdir tree that gets wedged and requires
manual recovery.  Add a sanity check to abort cleanly first.

Change-Id: Iad865ea860a3f1fd2f39ce683fe66bd4380745a5
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/244732
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-11-12 23:14:28 +00:00
dff919493a sync: report list of failing git trees
When repo sync fails because some git trees are not in clean state and
as such can not be rebased automatically, it is a pain to figure out
which trees are the culprits.

With this patch the list of offending trees is printed when repo sync
reports checkout errors.

TEST=ran 'repo sync' and observed the proper list of directories show
     up after the final error message

Bug: https://crbug.com/gerrit/11293
Change-Id: Icdf1a03e9014ecb184f331f513cc9a2efc7d11ed
Signed-off-by: Vadim Bendebury <vbendeb@google.com>
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/244053
Reviewed-by: Mike Frysinger <vapier@google.com>
2019-11-12 21:08:54 +00:00
3164d40e22 use open context managers in more places
Use open() as a context manager to simplify the close logic and make
the code easier to read & understand.  This is also more Pythonic.

Change-Id: I579d03cca86f99b2c6c6a1f557f6e5704e2515a7
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/244734
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-11-12 03:44:39 +00:00
f454512619 sync: make .git init more robust
Hitting Ctrl-C in the middle of this func will leave the .git in a
bad state that requires manual recovery.  The code tries to catch
all exceptions and recover by deleting the incomplete .git dir, but
it omits KeyboardInterrupt which Exception misses.

We could add that to the recovery path, but we can make this more
robust with a different approach: set up everything in .git.tmp/
and only move it to .git/ once we've fully initialized it.

Change-Id: I0f5b97f2e19fc39cffc3e5e23993a2da7220f4e3
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/244733
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-11-12 03:44:33 +00:00
b466854bed forall: add an --ignore-missing option
In CrOS, our infra has to deal with partial checkouts constantly
(for a variety of reasons).  To help reset back to a good state,
we run git commands via `repo forall`, but don't care about the
missing checkouts.  Add a flag so we can disambiguate between
missing repos and failing git subcommands.

Bug: https://crbug.com/1013377
Bug: https://crbug.com/1013623
Change-Id: Ie3498c6d111276c60d2ecedbba21bfa778588d50
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/241935
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-10-22 14:10:34 +00:00
d1e93dd58a python-support: adjust major versions
The plan previously documented was <=1.13.x is Python 2 and >=1.14.x
is Python 3.  Other projects that migrated Python versions and drop
support for older have tended to take a more drastic version jump to
make it clearer to users.  So lets adjust the plan to say <=1.x will
support Python 2, and >=2.x will be Python 3-only.

This also allows us to harmonize the repo launcher version.  It is
currently sitting at v1.26 and has been incremented independently of
the repo version for the life of the project.  While we might know
these lower nuances, pretty much no one else does and it just leads
to confusion: do I know version 1.26 or version 1.13.7?  Or do I
have both?  What does that even mean?

Once we update the major version to 2.0.0, we can also adjust the
launcher script to 2.0.0, and then the launcher release process will
be tied to a new repo release in general.

Bug: https://crbug.com/gerrit/10418
Change-Id: Idb2257371a06e56d2923cf717345c028f49176a2
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/240372
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-10-11 06:56:03 +00:00
e778e57f11 command: filter projects by active manifest groups
`repo forall <proj>` will look up all <proj> in the manifest for all
manifest groups regardless of which are active.  If <proj> is checked
out to different locations depending on the group, this ultimately
fails as we're unable to locate all of them.

Simple fix is to only include projects that match the manifest groups
that we already passed down & initialized to the active set, and that
we already use when getting the default project list.

Bug: https://crbug.com/gerrit/11677
Bug: https://crbug.com/1011226
Change-Id: I975f10f9a9e5a1cad7d87344123f8003732dab27
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/239652
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: Raul Rangel <rrangel@chromium.org>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
2019-10-08 20:15:08 +00:00
f1c5dd8a0f info: fix "current" output
The "Current revision" field shows the revision as listed in the
manifest.  I think most users expect this to show the revision
that the git tree is checked out to instead.  Switch the output
to show that revision instead, and add a "Current branch" if it
matches a local branch.

Change-Id: Ib546f5ebbc8a23875fbd14bf166fbe95b7dd244e
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/239240
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-10-05 05:24:34 +00:00
2058c63641 Only import imp on py2
imp is deprecatedon py3. It's also not used with py3, so just move it
to the py2 import block

Test: run `repo` command and verify warning is no longer present
Test: verify `repo sync` and `repo upload` function as expected
Change-Id: I9d59403d7819c4a478c9f54cbef114f8a96486a5
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/239713
Tested-by: Rashed Abdel-Tawab <rashedabdeltawab@gmail.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
2019-10-05 04:41:40 +00:00
c8290ad49e project: allow CurrentBranch to return None on errors
If the repo client checkout is in an incomplete sync state, the work
git repo might be in a bad way.  Turn errors parsing HEAD into None
since callers of CurrentBranch already need to account for it.

Change-Id: Ia7682e29ef4182006b1fb5f5e57800f8ab67a9f4
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/239239
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-10-01 05:53:35 +00:00
9775a3d5d2 info: allow NoSuchProjectError to bubble up
If the user passes in bad projects like `repo info asdf`, we currently
silently swallow those and do nothing.  Allow NoSuchProjectError to
bubble up to main which will handle & triage this correctly for us.

Change-Id: Ie04528e7b7a164293063a636813a73eaabdd5bc3
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/239238
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-10-01 05:53:09 +00:00
9bfdfbe117 version: add source versions & User-Agents to the output
Depending on where/how repo is invoked, the active version might be
from a git tree, and it might be different from the .repo/repo.git/
version in the current repo client checkout.  Report both if they're
different so it's clearer.

Lets also include the two different User-Agent's that we set up when
talking to networked services.

Bug: https://crbug.com/gerrit/11144
Change-Id: I2ebb6e3ac30e374a8406cab3e4438087246a8c57
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/239234
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-10-01 05:47:35 +00:00
2f0951b216 git_command: set GIT_HTTP_USER_AGENT on all requests
We've been setting the User-Agent header when making connections
from repo itself, but not when running git (as the latter will set
up User-Agent itself).  Our Gerrit/Git admins say it'll be helpful
if we pass through the repo version settings even when running git.

We currently set GIT_HTTP_USER_AGENT and not GIT_USER_AGENT as it's
unclear if the extended form works over all protocols.  We can wait
for a user request.

Bug: https://crbug.com/gerrit/11144
Change-Id: I21d293f49534058dbc23225152451df26c5b7bef
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/239233
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
2019-10-01 05:47:17 +00:00
72ab852ca5 grep: handle errors gracefully
If `git grep` fails in any project checkout (e.g. an incomplete
sync), make sure we print that error clearly rather than blowing
up, and exit non-zero in the process.

Bug: https://crbug.com/gerrit/11613
Change-Id: I31de1134fdcc7aaa9814cf2eb6a67d398eebf9cf
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/239237
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-10-01 05:45:58 +00:00
0a9265e2d6 diff: handle errors gracefully
If `git diff` fails in any project checkout (e.g. an incomplete
sync), make sure we print that error clearly rather than blowing
up, and exit non-zero in the process.

Bug: https://crbug.com/gerrit/11613
Change-Id: I12f278427cced20f23f8047e7e3dba8f442ee25e
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/239236
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-10-01 05:44:09 +00:00
dc1b59d2c0 forall: exit 1 if we skip any repos
If a repo doesn't exist (e.g. an incomplete sync), make sure we exit
non-zero when they get skipped.

Change-Id: Ifff711e374416b1e6b9b8da4fdc6f14b27ced450
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/239235
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-10-01 05:43:51 +00:00
71b0f312b1 git_command: refactor User-Agent settings
Convert the RepoUserAgent function into a UserAgent class.  This
makes it cleaner to hold internal state, and will make it easier
to add a separate git User-Agent, although we don't do it here.

We make the RepoSourceVersion independent of GitCommand so that
it can be called by the class (later).

Bug: https://crbug.com/gerrit/11144
Change-Id: Iab4e1f974b8733a36b243b2d03f5085a96effa19
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/239232
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-10-01 05:40:28 +00:00
369814b4a7 move UserAgent to git_command for wider user
We can't import the main module, so move the UserAgent helper out of
it and into the git_command module so it can be used in more places.

Bug: https://crbug.com/gerrit/11144
Change-Id: I8093c8a20bd1dc7d612d0e2a85180341817c0d86
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/231057
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
2019-10-01 05:39:27 +00:00
e37aa5f331 rebase: add basic coloring output
This uses coloring style like we use in grep/forall already.

Change-Id: I317e2e47567a30c513083c48e7c7c40b091bb29a
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/238555
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-09-24 04:17:03 +00:00
4a07798c82 rebase: add --fail-fast support
Lets switch the default rebase behavior to align with our new sync
behavior: we try to rebase all projects by default and exit/summarize
things at the very end if there were any errors.  Or if people want
to exit immediately, they can use the new --fail-fast option.

Change-Id: I436ac563f972b45de6ce9ad74da1e4870e584902
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/238553
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-09-24 02:06:26 +00:00
fb527e3f52 sync: create dedicated manifest project update func
Cut out some more standalone code from Execute to make this func a
bit more manageable.  The manifest project update is pretty simple
and standalone, but still takes up a good chunk of what's left.

Change-Id: Idc2442d9def495eccd0a49cda203c44aef16f129
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/236614
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
2019-09-19 01:52:08 +00:00
6be76337a0 repo: bump wrapper version
We've rolled quite a number of fixes since the last update, including
a lot of Python 3 improvements.  Lets bump the wrapper version for it.

Change-Id: I6c6c04c3c8241bf8e8bcf26603549ae4595fede8
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/237812
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-09-18 08:25:49 +00:00
a2cd6aeae8 Fix tag clobbering when -c is used.
Bug: b/140189154
Change-Id: I8861a6115b20c9a3d88ddec5344c75326ae44823
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/237572
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Xin Li <delphij@google.com>
2019-09-16 18:34:45 +00:00
70d861fa29 sync: improve output with intermingled progress bars and status
When displaying progress bars, we use \r to reset the cursor to the
start of the line before showing the new update.  This assumes the
new line will fully erase whatever was displayed there previously.
The "done" codepath tries to handle this by including a few extra
spaces at the end of the message to "white out" what was there.

Lets replace that hack with the standard ECMA escape sequence that
clears the current line completely.  This is the CSI "erase in line"
sequence that the terminal will use to delete all content.  The \r
is still needed to move the cursor to the start of the line.  Using
this sequence should be OK since we're already assuming the terminal
is ECMA compliant with our use of coloring sequences.  We also put
the \r after the CSI sequence on the off chance the terminal can't
process it and displays a few bytes of garbage.

The other improvement is to the syncbuffer API.  When it dumps its
status information, it almost always comes after a progress bar
update which leads to confusing comingled output.  Something like:
  Fetching projects: 100% (2/2) error: src/platform2/: branch ...
Since the progress bar is "throw away", have the syncbuffer reset
the current output to the start of the line before showing whatever
messages it has queued.

Bug: https://crbug.com/gerrit/11293
Change-Id: I6544d073fe993d98ee7e91fca5e501ba5fecfe4c
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/236615
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-09-13 02:58:07 +00:00
9100f7fadd repo: decode/encode all the subprocess streams
We use subprocess a lot in the wrapper, but we don't always read
or write the streams directly.  When we do, make sure we convert
to/from bytes before trying to use the content.

Change-Id: I318bcc8e7427998348e359f60c3b49e151ffbdae
Reported-by: Michael Scott <mike@foundries.io>
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/236612
Reviewed-by: Michael Scott <mike@foundries.io>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Michael Scott <mike@foundries.io>
2019-09-12 04:16:51 +00:00
01d6c3c0c5 sync: create dedicated smart sync func
The smart sync logic takes up about 45% of the overall Execute func
and is about 100 lines of code.  The only effect it has on the rest
of the code is to set the manifest_name variable.  Since this func
is already quite huge, split the smart sync logic out.

Change-Id: Id861849b0011ab47387d74e92c2ac15afcc938ba
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/234835
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
2019-09-11 23:24:11 +00:00
4c263b52e7 repo: fix unused variable usage
The refactoring here left behind a variable reference that no
longer exists.  Clean it up.

Bug: https://crbug.com/gerrit/11144
Change-Id: Ifdb7918b37864c48f3deef27c8bae3f793275d35
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/236613
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-09-11 17:57:49 +00:00
60fdc5cad1 Add repo start option to create the branch based off HEAD
This makes it way easier to recover from forgetting to run repo start
before committing: just run `repo start -b new-branch`, instead of
all that tedious mucking around with reflogs.

Change-Id: I56d49dce5d027e28fbba0507ac10cd763ccfc36d
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/232712
Reviewed-by: Mike Frysinger <vapier@google.com>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-09-04 04:34:50 +00:00
46702eddc7 sync: fix deprecated command line option -f
In commit d9e5cf0e ("sync: invert --force-broken with --fail-fast") the
force-broken option has been deprecated. Accidentally the option has
been changed from Boolean to Value. This breaks all users of repo with:

  main.py: error: -f option requires an argument

This is easy to avoid by keeping the type.

Signed-off-by: Stefan Müller-Klieser <s.mueller-klieser@phytec.de>
Change-Id: Ia8b589cf41ac756d10c61e17ec8d76ba8f7031f9
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/235043
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
2019-08-30 16:29:01 +00:00
ae6cb08ae5 split out cli validation from execution
A common pattern in our subcommands is to verify the arguments &
options before executing things.  For some subcommands, that check
stage is quite long which makes the execution function even bigger.
Lets split that logic out of the execute phase so it's easier to
manage these.

This is most noticeable in the sync subcommand whose Execute func
is quite large, and the option checking makes up ~15% of it.

The manifest command's Execute can be simplified significantly as
the optparse configuration always sets output_file to a string.

Change-Id: I7097847ff040e831345e63de6b467ee17609990e
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/234834
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-08-28 03:54:11 +00:00
3fc157285c add a --trace-python option
This can help debug issues by tracing all the repo python code with
the standard trace module.

Change-Id: Ibb7f4496ab6c7f9e130238ddf3a07c831952697a
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/234833
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
2019-08-27 18:44:17 +00:00
8a11f6f24c rename local trace module
There is a standard Python "trace" module, so having a local trace.py
prevents us being able to import that.  Rename the module to avoid.

Change-Id: I23e29ec95a2204bb168a641323d05e76968d9b57
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/234832
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-08-27 07:08:52 +00:00
898f4e6217 help: add a --help-all option to show all commands at once
This is useful when you want to scan all the possibilities of repo
at once.  Like when you're searching for different option names.

Change-Id: I225dfb94d2be78229905b744ecf57eb2829bb52d
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/232894
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-08-27 07:07:24 +00:00
d9e5cf0ee7 sync: invert --force-broken with --fail-fast
People seem to not expect the sync process to halt immediately if an
error is encountered.  It's also basically guaranteed to leave their
tree in an incomplete state.  Lets invert the default behavior so we
attempt to sync (both fetch & checkout) all projects.  If an error is
hit, we still exit(1) and show it at the end.

If people want the sync to abort quickly, they can use the new option
--fail-fast.

Bug: https://crbug.com/gerrit/11293
Change-Id: I49dd6c4dc8fd5cce8aa905ee169ff3cbe230eb3d
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/234812
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-08-27 01:20:44 +00:00
3069be2684 Explicitly allow clobbering tags when fetching from remote.
Bug: b/139860049
Change-Id: I3c4134eda7e9e75c9d72b233e269bcc0e624d1e8
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/234632
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Xin Li <delphij@google.com>
2019-08-22 18:33:41 +00:00
d5c306b404 rebase: pull out project-independent settings from the for loop
This makes the code a bit easier to read by doing all the project
independent settings first instead of repeating it for every for
loop iteration.

Change-Id: I4ff21296e444627beba2f4b86561069f5e9a0d73
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/233554
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-08-08 17:41:40 +00:00
a850ca2712 rebase/sync: use exit(1) for errors instead of exit(-1)
Callers don't actually see -1 (they'll usually see 255, but the exact
answer here is complicated).  Just switch to 1 as that's the standard
value tools use to indicate an error.

Change-Id: Ib712db1924bc3e5f7920bafd7bb5fb61f3bda44f
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/233553
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-08-08 05:07:31 +00:00
a34186e481 sync: fix handling of -f and local checkouts
The partial clone rework (commit 745be2ede1
"Add support for partial clone") changed the behavior when a single repo
hit a failure: it would always call sys.exit() immediately.  This isn't
even necessary as we already pass down an error event object which the
workers set and the parent checks.  Just delete the exit entirely.

Change-Id: Id72d8642aefa2bde24e1a438dbe102c3e3cabf48
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/233552
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-08-08 02:54:39 +00:00
600f49278a project: fix encoding handling with git commands
The GitCommand Wait helper takes care of decoding bytes to strings
for us.  That means we don't have to decode stdout ourselves which
is what our local rev list, ls-remote, and generic get_attr helpers
were doing.

If we don't use Wait though to capture the output but instead go
directly to the subprocess stdout, we do have to handle decoding
ourselves.  This is what the diff helpers were doing.

Bug: https://crbug.com/gerrit/10418
Change-Id: I057ca245af3ff18d6b4a074e3900887f06a5617d
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/233076
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-08-04 04:13:55 +00:00
1f2462e0d2 git_config: include project name in missing ref exception
When syncing in parallel, this exception is hard to trace back to
a specific repo as the relevant log line could have been pushed
out by other repos syncing code.

Change-Id: I382efeec7651e85622aa51e351134aef0148267f
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/233075
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Reviewed-by: Nasser Grainawi <nasser@codeaurora.org>
Tested-by: Mike Frysinger <vapier@google.com>
2019-08-03 16:15:48 +00:00
50d27639b5 manifest-format: document implicit directory creation w/<copyfile> & <linkfile>
Bug: https://crbug.com/gerrit/11218
Change-Id: Ie96b4c484d9fbfd550c580c3d02971dc088dd8b0
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/233052
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Jonathan Nieder <jrn@google.com>
2019-08-02 04:21:40 +00:00
c5b172ad6f manifest-format: clarify <copyfile> & <linkfile> restrictions
While we don't (yet) explicitly enforce all of these, make sure
we document the expected behavior so we can all agree on it.

Bug: https://crbug.com/gerrit/11218
Change-Id: Ife8298702fa445ac055ef43c6d62706a9cb199ce
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/232893
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-08-01 04:06:04 +00:00
87deaefd86 tests: add docstrings & print_function (for Python 3)
Bug: https://crbug.com/gerrit/10418
Change-Id: Id98183597a9b0201ca98ec0bf5033a5f5ac6bda2
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/232892
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
2019-08-01 03:03:48 +00:00
5fbd1c6053 wrapper: Fix indentation level
Change-Id: I6bee1771053fd8da9c135ed529c4926b42ee9f87
Signed-off-by: David Pursehouse <dpursehouse@collab.net>
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/232792
Reviewed-by: Jonathan Nieder <jrn@google.com>
2019-07-31 08:38:19 +00:00
1126c4ed86 wrapper: replace usage of deprecated imp module for Python 3
A warning is emitted

  DeprecationWarning: the imp module is deprecated in favour of
  importlib; see the module's documentation for alternative uses

Change-Id: I6c5a9e024a9a904e02a24331f615548be3fe5f8e
Signed-off-by: David Pursehouse <dpursehouse@collab.net>
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/230984
Reviewed-by: Jonathan Nieder <jrn@google.com>
2019-07-31 00:55:37 +00:00
f7c51606f0 hooks: support external hooks running different Python version
As we convert repo to support Python 3, the version of Python that we
use might not be the version that repo hooks users have written for.
Since repo upgrades are not immediate, and not easily under direct
control of end users (relative to the projects maintaining the hook
code), allow hook authors to declare the version of Python that they
want to use.

Now repo will read the shebang from the hook script and compare it
against the version of Python repo itself is running under.  If they
differ, we'll try to execute a separate instance of Python and have
it load & execute the hook.  If things are compatible, then we still
use the inprocess execution logic that we have today.

This allows repo hook users to upgrade on their own schedule (they
could even upgrade to Python 3 ahead of us) without having to worry
about their supported version being exactly in sync with repo's.

Bug: https://crbug.com/gerrit/10418
Change-Id: I97c7c96b64fb2ee465c39b90e9bdcc76394a146a
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/228432
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-07-27 01:10:40 +00:00
745be2ede1 Add support for partial clone.
A new option, --partial-clone is added to 'repo init' which tells repo
to utilize git's partial clone functionality, which reduces disk and
bandwidth usage when downloading by omitting blob downloads initially.
Different from restricting clone-depth, the user will have full access
to change history, etc., as the objects are downloaded on demand.

Change-Id: I60326744875eac16521a007bd7d5481112a98749
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/229532
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Xin Li <delphij@google.com>
2019-07-16 00:23:16 +00:00
87fb5a1894 repo/main: add module docstrings
This should help people get some bearings in the codebase.

Change-Id: I951238fe617a3ecb04a47ead3809ec72c8fbf5a1
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/231232
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-07-12 17:23:53 +00:00
ab85fe7c53 use print() instead of sys.stdout.write()
We're relying on sys.stdout.write() to flush its buffer which isn't
guaranteed, and is not the case in Python 3.  Change to use print()
everywhere to be standard, and utilize the end= keyword to get the
EOL semantics we need.

We can't use print's flush= keyword as that's only in Python 3.
Leave behind a TODO to clean it up when we can drop Python 2.

Bug: https://crbug.com/gerrit/10418
Change-Id: I562128c7f1e6d154f4a6ecdf33a70fa2811dc2af
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/230392
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
2019-07-11 06:26:40 +00:00
4f42a97067 run_tests: add a helper for invoking unittests
This makes it very easy for people to run all our unittests with just
`./run_tests`.  There doesn't seem to be any other way currently to
quickly invoke any of the tests.

Change-Id: I1f9a3745fa397a1e797bd64065c2ba7f338de4a1
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/227613
Tested-by: David Pursehouse <dpursehouse@collab.net>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
2019-07-11 04:34:08 +00:00
2b7daff8cb Don't try to decode when checking clone bundles
This fix exception with python3 with stack-trace:

error: Cannot fetch platform_external_grpc-grpc-java.git (UnicodeDecodeError: 'utf-8' codec can't decode byte 0x96 in position 640: invalid start byte)

[...]
  File "[...]project.py", line 2255, in _IsValidBundle
    if f.read(16) == '# v2 git bundle\n':
  File "/usr/lib/python3.5/codecs.py", line 321, in decode
    (result, consumed) = self._buffer_decode(data, self.errors, final)

Even if we ask 16 characters, python buffered decoder will try to decode more in the buffer

The patch works for python2 and python3, and open the file in byte mode so that decoding is not attemped

Signed-off-by: Pierre Tardy <tardyp@gmail.com>
Change-Id: I837ae3c5cd724b34670fc2a84e853431f482b20d
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/224642
Reviewed-by: Mike Frysinger <vapier@google.com>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-07-11 01:33:37 +00:00
242fcdd93b main: user-agent: include full git version info
We've been truncating the git version info in the user agent to the
first three components.  So given an example `git --version` like
"2.22.0.510.g264f2c817a-goog", we were cutting it down to "2.22.0".
For user-agent purposes, we usually want that full string, so use
the original full value instead.

Bug: https://crbug.com/gerrit/11144
Change-Id: I8ffe3186bdaac96164c34ac835a54bb3fc85527e
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/231056
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-07-11 01:32:38 +00:00
ca540aed19 git_command: drop custom version helper
Since ParseGitVersion can call `git --version` automatically, we don't
need this duplicate version() helper anymore.  The only other user is
the `repo version` code, so convert that to version_tuple().full.

Bug: https://crbug.com/gerrit/11144
Change-Id: I9d77822fc39f4ba28884d9183359169cabf5f17d
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/231055
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-07-11 01:30:18 +00:00
f88b2fe569 repo: all ParseGitVersion to load git version info itself
All code that calls ParseGitVersion needs to run `git --version`
itself and parse the output before passing it in.  To avoid that
duplication, allow ParseGitVersion to run `git --version` itself
if ver_str=None.

Bug: https://crbug.com/gerrit/11144
Change-Id: Ie07793ca57a40c0231af808df04a576118d5eea3
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/231054
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
2019-07-11 01:29:18 +00:00
6db1b9e282 repo: return a namedtuple with full version info
We were returning an e.g. tuple(1,2,3), but that strips off the full
version string which we might want in some places e.g. '1.2.3-rc3'.
Change the return value to a namedtuple so we can pass back up the
full version string.  For code doing a compare with three elements
(all code today), things still work fine as the namedtuple will DTRT
in this scenario.

Bug: https://crbug.com/gerrit/11144
Change-Id: Ib897b5df308116ad1550b0cf18f49afeb662423e
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/231053
Reviewed-by: David Pursehouse <dpursehouse@collab.net>
Tested-by: Mike Frysinger <vapier@google.com>
2019-07-11 01:28:14 +00:00
490e16385d Remove double forall from "repo help forall" output
%prog represents the full subcommand ("repo" + subcommand name), not a
Windows-style environment variable for "repo". The current help output
shows

 repo forall% forall ...

Correct the variable usage so it shows "repo forall ..." instead.

Change-Id: I1fea55572428cc922ddf24ace1168a3d8f82dad0
2019-07-08 22:42:38 +00:00
ec558df074 fix raise syntax
This takes a single argument (the error message), not multiple
arguments that get formatted implicitly.

Change-Id: Idfbc913ea9f93820edb7e955e9e4f57618c8cd1b
2019-07-05 01:38:14 -04:00
81f5c59671 project: rev_list: simplify execution
Currently we read the binary stream from the subprocess code directly
before waiting for it to finish, but there's no need to do so as we
aren't streaming the output to the user.  This also means we pass up
binary data to the caller as we don't go through GitCommand's internal
logic which decodes the stream as utf-8.

Simplify the code by calling Wait first, then splitting the entire
captured output in one line.

Bug: https://crbug.com/gerrit/10418
Change-Id: I7a57904be8cb546a229980fb79c829fc3df31e7d
2019-07-05 05:31:38 +00:00
1b9adab75a handle binary stream from urllib.request.urlopen
Python 3 returns bytes by default with urlopen.  Adjust our code to
handle that scenario and decode as necessary.

Bug: https://crbug.com/gerrit/10418
Change-Id: Icf4cd80e7ef92d71a3eefbc6113f1ba11c32eebc
2019-07-04 18:19:00 -04:00
3698ab7c92 Support clone bundle through persistent-http[s].
Bug: https://crbug.com/gerrit/11075
Change-Id: I367c6bfe8da47d886c017a2ac614d4ccb3f8a438
2019-06-26 09:42:21 -07:00
0c0e934b69 sync: use integer division with job counts
Neither of the fields here expect floats so make sure we use integer
division when calculating things.

Bug: https://crbug.com/gerrit/10418
Change-Id: Ibda068b16a7bba7ff3efba442c4bbff4415caa6e
2019-06-14 14:47:01 +00:00
9e71842fbf status: import print_function
This module uses print() so make sure we import the print function.
It doesn't really impact the current code due to the simple way it
is calling print, but we should be sane to avoid future issues.

Change-Id: I0b15344678c1dcb71207faa333c239b3fced1d62
2019-06-14 14:13:23 +00:00
61b2d41f26 add license header to a few more files
Change-Id: I24e6b1df5f15a8e71c0f4a9edac505a8902ec267
2019-06-13 13:23:19 -04:00
da9e200f1d repo: drop Python 3 warning
Lets get people to start filing bugs :).

Bug: https://crbug.com/gerrit/10418
Change-Id: I1d55bf0c60dbdbd6537d30b2cf9ea91d2928e387
2019-06-13 14:32:03 +00:00
c92ce5c7dc repo: restore use of print_function
We avoided this future import because Python 2.4 & 2.5 did not
support it.  We've dropped support for Python 2.6 at this point,
and those versions are long dead.  Since this workaround adds a
bit of complexity to the codebase, drop it.  Considering we are
not running any actual tests against older versions, there's no
sense in trying to support them anymore.

Change-Id: Icda874861e8a8eb4fa07c624a9e7c5ee2a0da401
2019-06-13 14:31:45 +00:00
f601376e13 set default file encoding to utf-8
There's no reason to support any other encoding in these files.
This only affects the files themselves and not streams they open.

Bug: https://crbug.com/gerrit/10418
Change-Id: I053cb40cd3666ce5c8a0689b9dd938f24ca765bf
2019-06-13 14:30:52 +00:00
31067c0ac5 tweak raise/dict syntax for Python 3 compat
Use the `raise` statement directly.

Switch to using .items() instead of .iteritems().  Python 3 doesn't
have .iteritems() as .items() is a generator, and these are small
enough that the Python 2 overhead should be negligible.

We have to run .keys() through list() in a few places as Python 3
uses a generator and we sometimes want to iterate more than once.
That's why we don't change all .keys() or .items() calls -- most
are in places where generators are fine.

Bug: https://crbug.com/gerrit/10418
Change-Id: I469899d9b77ffd77ccabb831bc4b217407fefe6f
2019-06-13 13:39:25 +00:00
35159abbeb repo: standardize help behavior
Standard utilities exit normally/zero when users explicitly request
--help, and they write to stdout.  Exiting non-zero & using stderr
is meant for incorrect tool usage instead.  We're already doing this
for `repo help <init|gitc-init>` calls, so lets fix `repo help` and
`repo --help|-h` to match.

Change-Id: Ia4f352b431c91eefef70dcafc11f00209ee69809
2019-06-13 13:34:54 +00:00
24ee29e468 wrapper: drop shebang
This isn't executable (+x), nor does it have a main func or code
that would run if it were.  It's simply an imported module like
most others in here.  Drop the shebang to avoid confusion.

Change-Id: I5e2881eb1de5e809a3fa9e8f49220ed797034fb1
2019-06-13 02:00:17 -04:00
1b291fc2e7 Merge "docs: start a release document" 2019-06-13 05:57:59 +00:00
a26c49ead4 docs: start a release document
Change-Id: I884639665c020338ec9ceeb1add5c3b862583674
2019-06-12 23:18:10 -04:00
c745350ab9 diffmanifests: honor user-supplied manifest paths
The current implementation ignores the user-specified paths to
manifests.  if the "repo diffmanifests" is invoked with absolute
file paths for one or both manifests, the command fails with message:

fatal: duplicate path ... in /tmp/manifest-old.xml

Also the current implementation fails to expand the absolute path to
manifest files if "repo diffmanifests" is invoked with relative
paths, i.e "repo diffmanifests manifest-old.xml manifest-new.xml".

fatal: manifest manifest-old.xml not found

This commit fixes the first issue by disabling the local manifest
discovery for diffmanifests command, and the second issue by
expanding paths to manifests within "diffmanifests" sub-command.

Test: repo manifest --revision-as-HEAD -o /tmp/manifest-old.xml
      repo sync
      repo manifest --revision-as-HEAD -o /tmp/manifest-new.xml
      repo diffmanifests /tmp/manifest-old.xml /tmp/manifest-new.xml
Change-Id: Ia125d769bfbea75adb9aba81abbd8c636f2168d4
Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
2019-06-06 07:36:10 +00:00
025704e946 Merge "platform_utils_win32: remove an unnecessary workaround" 2019-06-04 16:28:15 +00:00
c5b0e23490 project: Set config option to skip lfs process filter
During sync, repo runs `git read-tree --reset -u -v HEAD` which causes
git-lfs's smudge filter to run, which fails because git-lfs does not
work with bare repositories.

This was fixed in I091ff37998131e2e6bbc59aa37ee352fe12d7fcd to
automatically disable this smudge filter. However, later versions of
Git (2.11.0) introduced a new filter protocol [1], to avoid spawning
a new command for each filtered file. This was implemented in Git-LFS
1.5.0 [2].

This patch fixes the issue by setting the git lfs process filter, in
addition to the smudge filter. For any projects that have LFS objects,
`git lfs pull` must still be executed manually afterwards.

[1] edcc85814c
[2] https://github.com/git-lfs/git-lfs/pull/1617

Bug: https://crbug.com/gerrit/10911
Change-Id: I277fc68fdefc91514a2412b3887e3be9106cab48
2019-05-24 12:06:33 +00:00
d92464e8ef Honor --depth during repo init
If a user is asking for a shallow clone of the repos, they probably
expect a shallow clone of the manifest repo too. For very large
manifest repos, this can be a huge space and time savings. For one real-world
repo, a 'repo init --no-tags --current-branch' used 350MB of disk space and
took 7 minutes. Adding --depth 1 and this change reduced it to 10MB and 2.5
minutes.

Change-Id: I6fa662e174e623ede8861efc862ce26d65d4958d
2019-05-21 10:47:21 -06:00
0968570df2 Merge "Print project name when work tree initialization fails" 2019-05-16 23:05:53 +00:00
f25a370a14 Use %topic=topic instead of deprecated /topic syntax on push
Bug: https://crbug.com/gerrit/9930
Change-Id: Iefa202d42ef6e6b8b2b1a3f9b8baa5f0d65cbd60
2019-05-15 10:59:53 +02:00
b554838ce8 Print project name when work tree initialization fails
When syncing a lot of projects in parallel, it is not otherwise
clear which one of them has failed to init work tree.

Change-Id: I8edfb4955023389a499e99cfa511bdc0d2850ba2
2019-05-09 13:07:20 -07:00
2d095da4f1 Ignore submodules when calculating 'git diff-files'
This allows projects to include submodules inside of
projects that use repo without repo incorrectly believing
the area is dirty just because a submodule has updates.
This is in line with git porcelain commands which generally
require a commandline flag to include submodules (git add,
git rebase).

Change-Id: Ide8a292162a42ab35145b5c4ca8ca0d020cdfe81
2019-05-02 18:21:42 -07:00
266f74c888 info: Use the non-formatting printer for headtext
If "repo init" was run in a path containing "%", "repo info" would fail
printing the path with

    File ".repo/repo/color.py", line 173, in f
      return fmt % args
    TypeError: not enough arguments for format string

as the "%" in the path name is interpreted as the start of a formatting
specifier. Avoid that by using the non-formatting printer for headtext
which does not require any formatting so there is no need to try to
expand "%" sequences.

Change-Id: Ie193b912191fe7cdabdce5c97bb100f0714f6e76
Signed-off-by: Sebastian Schuberth <sschuberth@gmail.com>
2019-04-17 19:46:05 +02:00
1f1596b473 Don't print "persistent ref" message when syncing quietly
The newly introduced "Already have persistent ref" message prevents
repo from overwriting the last line when syncing quietly. Omit the
message when syncing quietly to clean up the output and to restore
the previous behaviour.

Change-Id: Idf42751c67f95924d6de50092ba54d4c6fe93096
2019-04-15 14:37:17 +02:00
0d9b16d1d8 sync: deleted unused repos in reversed order (children before parent)
Bug: chromium:950002
Test: repo sync chromeos using release manifest file
Change-Id: I613df6a1973eb36acd806a703e72f5172554bcc7
2019-04-06 00:49:47 +08:00
a84df06160 platform_utils_win32: remove an unnecessary workaround
The comment in _create_symlink is incorrect. The return value of
CreateSymbolicLink is as documented, it was just declared with
the wrong return type. The actual return type is BOOLEAN, not BOOL.

Fixing this allows us to simplify the code a bit.

Change-Id: I4d2190a50d45ba41dd9814bf7079a5784fc0a366
2019-03-21 23:45:59 +03:00
e57f1146de sync: respect --force-sync when fetching updates
If a tag is rewritten on the server (which is bad), trying to recover
locally with `repo sync --force-sync` doesn't actually work.  People
have to manually delete things themselves to fix syncing.  While tags
should never be rewritten in practice, allow users to easily recover
from broken servers.

We updated some of these code paths already (see commit 6e53844f1e
"Allow clobbering of existing tags from remote."), but the incremental
update flow was missed.

Bug: b/120778183
Bug: chromium:932651
Test: delete local tag & recreate to diff commit, then check
      `repo sync` & `repo sync --force-sync` behavior
Change-Id: I3648f7d2526732c06016b691a9a36c003157618d
2019-03-18 21:31:03 -04:00
01019d94af docs: fixed typo error.
Change-Id: Ic3ec1bfb150ec932e05ba5eda43537784f1fdcda
2019-03-18 13:38:33 +09:00
834d308a2b Merge "project: Relax the submodule name pattern to accept dots" 2019-03-14 07:14:28 +00:00
c18ee35da6 Merge "sync: Add option '--force-remove-dirty'" 2019-03-11 17:56:00 +00:00
d3c0f5914f sync: Add option '--force-remove-dirty'
Forcefully remove dirty projects if option '--force-remove-dirty' is given.
The '--force-remove-dirty' option can be used to remove previously used
projects with uncommitted changes. WARNING: This may cause data to be lost
since uncommitted changes may be removed with projects that no longer exist
in the manifest.

Change-Id: I844a6e943ded522fdc7b1b942c0a1269768054bc
2019-03-11 13:06:49 -04:00
41a26837d0 project: Relax the submodule name pattern to accept dots
Even if dots are used as separators for Git config keys, they are not
forbidden as part of submodule names. This fixes the issue of submodules
with a name like e.g. "long.js" to be skipped from checkout.

Change-Id: I77da07925ad207fa3d043067dfbbcb4a1ebdac4d
2019-03-11 13:53:38 +01:00
e7379dc5f7 docs: document a Python 3 migration plan
Bug: https://crbug.com/gerrit/10418
Change-Id: I72d82ce3a2d9af45d942bb10de82340110864ea5
2019-02-01 03:06:04 -05:00
13f323b2c2 event_log: turn id generation from a generator to a func call
Running lots of sync processes in parallel can hit the failure:
Fetching projects:  23% (124/523)Exception in thread Thread-201:
Traceback (most recent call last):
  File "/usr/lib/python2.7/threading.py", line 801, in __bootstrap_inner
    self.run()
  File "/usr/lib/python2.7/threading.py", line 754, in run
    self.__target(*self.__args, **self.__kwargs)
  File "/usr/local/src/repo/subcmds/sync.py", line 278, in _FetchProjectList
    success = self._FetchHelper(opt, project, *args, **kwargs)
  File "/usr/local/src/repo/subcmds/sync.py", line 357, in _FetchHelper
    start, finish, success)
  File "/usr/local/src/repo/event_log.py", line 104, in AddSync
    event = self.Add(project.relpath, task_name, start, finish, success)
  File "/usr/local/src/repo/event_log.py", line 74, in Add
    'id': (kind, next(self._next_id)),
ValueError: generator already executing

It looks like, while we lock the multiprocessing value correctly, the
generator that wraps the value isn't parallel safe.  Since we don't
have a way of doing that (as it's part of the language), turn it into
a plain function call instead.

Bug: https://crbug.com/gerrit/10293
Change-Id: I0db03601986ca0370a1699bab32adb03e7b2910a
2019-01-14 16:11:08 -05:00
12ee5446e9 init: Remove -c short option for --current-branch
This option conflicts with the gitc-init -c short option.

Bug: https://crbug.com/gerrit/10200
Change-Id: I06f37564429ca0bd4c0bbea6066daae4f663c838
2018-12-20 19:55:02 +00:00
e158e3802d Merge "README: link in new bug tracker" 2018-12-20 07:21:02 +00:00
3bbbcaf99d README: link in new bug tracker
Change-Id: I043afc0b77e709919e49ce548dff47776fddaddf
2018-12-20 02:11:46 -05:00
d4b13c280b Leverage the next keyword from python 2.7
This is literally what the next keyword is for.
https://www.python.org/dev/peps/pep-3114/

Change-Id: I843755910b847737b077ff2361ba3e04409db0f0
2018-12-19 11:06:35 -08:00
71 changed files with 3298 additions and 918 deletions

8
.gitignore vendored
View File

@ -1,3 +1,11 @@
*.egg-info/
*.log
*.pyc
__pycache__
/dist
.repopickle_*
/repoc
/.tox
# PyCharm related
/.idea/

View File

@ -5,6 +5,6 @@
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
<path>/git-repo</path>
</pydev_pathproperty>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.6</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
</pydev_project>

View File

6
MANIFEST.in Normal file
View File

@ -0,0 +1,6 @@
graft docs hooks tests
include *.py
include LICENSE
include git_ssh
include repo
include run_tests

View File

@ -7,10 +7,30 @@ easier to work with Git. The repo command is an executable Python script
that you can put anywhere in your path.
* Homepage: https://gerrit.googlesource.com/git-repo/
* Bug reports: https://code.google.com/p/git-repo/issues/
* Bug reports: https://bugs.chromium.org/p/gerrit/issues/list?q=component:repo
* Source: https://gerrit.googlesource.com/git-repo/
* Overview: https://source.android.com/source/developing.html
* Docs: https://source.android.com/source/using-repo.html
* [repo Manifest Format](./docs/manifest-format.md)
* [repo Hooks](./docs/repo-hooks.md)
* [Submitting patches](./SUBMITTING_PATCHES.md)
* Running Repo in [Microsoft Windows](./docs/windows.md)
## Install
Many distros include repo, so you might be able to install from there.
```sh
# Debian/Ubuntu.
$ sudo apt-get install repo
# Gentoo.
$ sudo emerge dev-vcs/repo
```
You can install it manually as well as it's a single script.
```sh
$ mkdir -p ~/.bin
$ PATH="${HOME}/.bin:${PATH}"
$ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/.bin/repo
$ chmod a+rx ~/.bin/repo
```

View File

@ -1,3 +1,5 @@
[TOC]
# Short Version
- Make small logical changes.
@ -52,17 +54,57 @@ Run `flake8` on changes modules:
flake8 file.py
Note that repo generally follows [Google's python style guide]
(https://google.github.io/styleguide/pyguide.html) rather than [PEP 8]
(https://www.python.org/dev/peps/pep-0008/), so it's possible that
the output of `flake8` will be quite noisy. It's not mandatory to
avoid all warnings, but at least the maximum line length should be
followed.
Note that repo generally follows [Google's python style guide] rather than
[PEP 8], so it's possible that the output of `flake8` will be quite noisy.
It's not mandatory to avoid all warnings, but at least the maximum line
length should be followed.
If there are many occurrences of the same warning that cannot be
avoided without going against the Google style guide, these may be
suppressed in the included `.flake8` file.
[Google's python style guide]: https://google.github.io/styleguide/pyguide.html
[PEP 8]: https://www.python.org/dev/peps/pep-0008/
## Running tests
We use [pytest](https://pytest.org/) and [tox](https://tox.readthedocs.io/) for
running tests. You should make sure to install those first.
To run the full suite against all supported Python versions, simply execute:
```sh
$ tox -p auto
```
We have [`./run_tests`](./run_tests) which is a simple wrapper around `pytest`:
```sh
# Run the full suite against the default Python version.
$ ./run_tests
# List each test as it runs.
$ ./run_tests -v
# Run a specific unittest module (and all tests in it).
$ ./run_tests tests/test_git_command.py
# Run a specific testsuite in a specific unittest module.
$ ./run_tests tests/test_editor.py::EditString
# Run a single test.
$ ./run_tests tests/test_editor.py::EditString::test_cat_editor
# List all available tests.
$ ./run_tests --collect-only
# Run a single test using substring match.
$ ./run_tests -k test_cat_editor
```
The coverage isn't great currently, but it should still be run for all commits.
Adding more unittests for changes you make would be greatly appreciated :).
Check out the [tests/](./tests/) subdirectory for more details.
## Check the license
repo is licensed under the Apache License, 2.0.

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#
@ -97,6 +98,16 @@ class Command(object):
self.OptionParser.print_usage()
sys.exit(1)
def ValidateOptions(self, opt, args):
"""Validate the user options & arguments before executing.
This is meant to help break the code up into logical steps. Some tips:
* Use self.OptionParser.error to display CLI related errors.
* Adjust opt member defaults as makes sense.
* Adjust the args list, but do so inplace so the caller sees updates.
* Try to avoid updating self state. Leave that to Execute.
"""
def Execute(self, opt, args):
"""Perform the action, after option parsing is complete.
"""
@ -164,7 +175,10 @@ class Command(object):
self._ResetPathToProjectMap(all_projects_list)
for arg in args:
projects = manifest.GetProjectsWithName(arg)
# We have to filter by manifest groups in case the requested project is
# checked out multiple times or differently based on them.
projects = [project for project in manifest.GetProjectsWithName(arg)
if project.MatchesGroups(groups)]
if not projects:
path = os.path.abspath(arg).replace('\\', '/')
@ -189,7 +203,7 @@ class Command(object):
for project in projects:
if not missing_ok and not project.Exists:
raise NoSuchProjectError(arg)
raise NoSuchProjectError('%s (%s)' % (arg, project.relpath))
if not project.MatchesGroups(groups):
raise InvalidProjectGroupsError(arg)

145
docs/internal-fs-layout.md Normal file
View File

@ -0,0 +1,145 @@
# Repo internal filesystem layout
A reference to the `.repo/` tree in repo client checkouts.
Hopefully it's complete & up-to-date, but who knows!
*** note
**Warning**:
This is meant for developers of the repo project itself as a quick reference.
**Nothing** in here must be construed as ABI, or that repo itself will never
change its internals in backwards incompatible ways.
***
[TOC]
## .repo/ layout
All content under `.repo/` is managed by `repo` itself with few exceptions.
In general, you should not make manual changes in here.
If a setting was initialized using an option to `repo init`, you should use that
command to change the setting later on.
It is always safe to re-run `repo init` in existing repo client checkouts.
For example, if you want to change the manifest branch, you can simply run
`repo init --manifest-branch=<new name>` and repo will take care of the rest.
### repo/ state
* `repo/`: A git checkout of the repo project. This is how `repo` re-execs
itself to get the latest released version.
It tracks the git repository at `REPO_URL` using the `REPO_REV` branch.
Those are specified at `repo init` time using the `--repo-url=<REPO_URL>`
and `--repo-branch=<REPO_REV>` options.
Any changes made to this directory will usually be automatically discarded
by repo itself when it checks for updates. If you want to update to the
latest version of repo, use `repo selfupdate` instead. If you want to
change the git URL/branch that this tracks, re-run `repo init` with the new
settings.
* `.repo_fetchtimes.json`: Used by `repo sync` to record stats when syncing
the various projects.
### Manifests
For more documentation on the manifest format, including the local_manifests
support, see the [manifest-format.md] file.
* `manifests/`: A git checkout of the manifest project. Its `.git/` state
points to the `manifest.git` bare checkout (see below). It tracks the git
branch specified at `repo init` time via `--manifest-branch`.
The local branch name is always `default` regardless of the remote tracking
branch. Do not get confused if the remote branch is not `default`, or if
there is a remote `default` that is completely different!
No manual changes should be made in here as it will just confuse repo and
it won't automatically recover causing no new changes to be picked up.
* `manifests.git/`: A bare checkout of the manifest project. It tracks the
git repository specified at `repo init` time via `--manifest-url`.
No manual changes should be made in here as it will just confuse repo.
If you want to switch the tracking settings, re-run `repo init` with the
new settings.
* `manifest.xml -> manifests/<manifest-name>.xml`: A symlink to the manifest
that the user wishes to sync. It is specified at `repo init` time via
`--manifest-name`.
Do not try to repoint this symlink to other files as it will confuse repo.
If you want to switch manifest files, re-run `repo init` with the new
setting.
* `manifests.git/.repo_config.json`: JSON cache of the `manifests.git/config`
file for repo to read/process quickly.
* `local_manifest.xml` (*Deprecated*): User-authored tweaks to the manifest
used to sync. See [local manifests] for more details.
* `local_manifests/`: Directory of user-authored manifest fragments to tweak
the manifest used to sync. See [local manifests] for more details.
### Project objects
* `project.list`: Tracking file used by `repo sync` to determine when projects
are added or removed and need corresponding updates in the checkout.
* `projects/`: Bare checkouts of every project synced by the manifest. The
filesystem layout matches the `<project path=...` setting in the manifest
(i.e. where it's checked out in the repo client source tree). Those
checkouts will symlink their `.git/` state to paths under here.
Some git state is further split out under `project-objects/`.
* `project-objects/`: Git objects that are safe to share across multiple
git checkouts. The filesystem layout matches the `<project name=...`
setting in the manifest (i.e. the path on the remote server). This allows
for multiple checkouts of the same remote git repo to share their objects.
For example, you could have different branches of `foo/bar.git` checked
out to `foo/bar-master`, `foo/bar-release`, etc... There will be multiple
trees under `projects/` for each one, but only one under `project-objects/`.
This can run into problems if different remotes use the same path on their
respective servers ...
* `subprojects/`: Like `projects/`, but for git submodules.
* `subproject-objects/`: Like `project-objects/`, but for git submodules.
### Settings
The `.repo/manifests.git/config` file is used to track settings for the entire
repo client checkout.
Most settings use the `[repo]` section to avoid conflicts with git.
User controlled settings are initialized when running `repo init`.
| Setting | `repo init` Option | Use/Meaning |
|-------------------|---------------------------|-------------|
| manifest.groups | `--groups` & `--platform` | The manifest groups to sync |
| repo.archive | `--archive` | Use `git archive` for checkouts |
| repo.clonefilter | `--clone-filter` | Filter setting when using [partial git clones] |
| repo.depth | `--depth` | Create shallow checkouts when cloning |
| repo.dissociate | `--dissociate` | Dissociate from any reference/mirrors after initial clone |
| repo.mirror | `--mirror` | Checkout is a repo mirror |
| repo.partialclone | `--partial-clone` | Create [partial git clones] |
| repo.reference | `--reference` | Reference repo client checkout |
| repo.submodules | `--submodules` | Sync git submodules |
| user.email | `--config-name` | User's e-mail address; Copied into `.git/config` when checking out a new project |
| user.name | `--config-name` | User's name; Copied into `.git/config` when checking out a new project |
[partial git clones]: https://git-scm.com/docs/gitrepository-layout#_code_partialclone_code
## ~/ dotconfig layout
Repo will create & maintain a few files in the user's home directory.
* `.repoconfig/`: Repo's per-user directory for all random config files/state.
* `.repoconfig/keyring-version`: Cache file for checking if the gnupg subdir
has all the same keys as the repo launcher. Used to avoid running gpg
constantly as that can be quite slow.
* `.repoconfig/gnupg/`: GnuPG's internal state directory used when repo needs
to run `gpg`. This provides isolation from the user's normal `~/.gnupg/`.
* `.repo_.gitconfig.json`: JSON cache of the `.gitconfig` file for repo to
read/process quickly.
[manifest-format.md]: ./manifest-format.md
[local manifests]: ./manifest-format.md#Local-Manifests

View File

@ -66,7 +66,7 @@ following DTD:
<!ATTLIST project groups CDATA #IMPLIED>
<!ATTLIST project sync-c CDATA #IMPLIED>
<!ATTLIST project sync-s CDATA #IMPLIED>
<!ATTLIST default sync-tags CDATA #IMPLIED>
<!ATTLIST project sync-tags CDATA #IMPLIED>
<!ATTLIST project upstream CDATA #IMPLIED>
<!ATTLIST project clone-depth CDATA #IMPLIED>
<!ATTLIST project force-path CDATA #IMPLIED>
@ -89,6 +89,7 @@ following DTD:
<!ATTLIST extend-project path CDATA #IMPLIED>
<!ATTLIST extend-project groups CDATA #IMPLIED>
<!ATTLIST extend-project revision CDATA #IMPLIED>
<!ATTLIST extend-project remote CDATA #IMPLIED>
<!ELEMENT remove-project EMPTY>
<!ATTLIST remove-project name CDATA #REQUIRED>
@ -306,6 +307,9 @@ belongs. Same syntax as the corresponding element of `project`.
Attribute `revision`: If specified, overrides the revision of the original
project. Same syntax as the corresponding element of `project`.
Attribute `remote`: If specified, overrides the remote of the original
project. Same syntax as the corresponding element of `project`.
### Element annotation
Zero or more annotation elements may be specified as children of a
@ -322,13 +326,29 @@ Zero or more copyfile elements may be specified as children of a
project element. Each element describes a src-dest pair of files;
the "src" file will be copied to the "dest" place during `repo sync`
command.
"src" is project relative, "dest" is relative to the top of the tree.
Copying from paths outside of the project or to paths outside of the repo
client is not allowed.
"src" and "dest" must be files. Directories or symlinks are not allowed.
Intermediate paths must not be symlinks either.
Parent directories of "dest" will be automatically created if missing.
### Element linkfile
It's just like copyfile and runs at the same time as copyfile but
instead of copying it creates a symlink.
The symlink is created at "dest" (relative to the top of the tree) and
points to the path specified by "src" which is a path in the project.
Parent directories of "dest" will be automatically created if missing.
The symlink target may be a file or directory, but it may not point outside
of the repo client.
### Element remove-project
Deletes the named project from the internal manifest table, possibly

47
docs/python-support.md Normal file
View File

@ -0,0 +1,47 @@
# Supported Python Versions
With Python 2.7 officially going EOL on [01 Jan 2020](https://pythonclock.org/),
we need a support plan for the repo project itself.
Inevitably, there will be a long tail of users who still want to use Python 2 on
their old LTS/corp systems and have little power to change the system.
## Summary
* Python 3.6 (released Dec 2016) is required by default starting with repo-2.x.
* Older versions of Python (e.g. v2.7) may use the legacy feature-frozen branch
based on repo-1.x.
## Overview
We provide a branch for Python 2 users that is feature-frozen.
Bugfixes may be added on a best-effort basis or from the community, but largely
no new features will be added, nor is support guaranteed.
Users can select this during `repo init` time via the [repo launcher].
Otherwise the default branches (e.g. stable & master) will be used which will
require Python 3.
This means the [repo launcher] needs to support both Python 2 & Python 3, but
since it doesn't import any other repo code, this shouldn't be too problematic.
The master branch will require Python 3.6 at a minimum.
If the system has an older version of Python 3, then users will have to select
the legacy Python 2 branch instead.
### repo hooks
Projects that use [repo hooks] run on independent schedules.
They might migrate to Python 3 earlier or later than us.
To support them, we'll probe the shebang of the hook script and if we find an
interpreter in there that indicates a different version than repo is currently
running under, we'll attempt to reexec ourselves under that.
For example, a hook with a header like `#!/usr/bin/python2` will have repo
execute `/usr/bin/python2` to execute the hook code specifically if repo is
currently running Python 3.
For more details, consult the [repo hooks] documentation.
[repo hooks]: ./repo-hooks.md
[repo launcher]: ../repo

251
docs/release-process.md Normal file
View File

@ -0,0 +1,251 @@
# repo release process
This is the process for creating a new release of repo, as well as all the
related topics and flows.
[TOC]
## Launcher script
The main repo script serves as a standalone program and is often referred to as
the "launcher script".
This makes it easy to copy around and install as you don't have to install any
other files from the git repo.
Whenever major changes are made to the launcher script, you should increment the
`VERSION` variable in the launcher itself.
At runtime, repo will check this to see if it needs to be updated (and notify
the user automatically).
## Key management
Every release has a git tag that is signed with a key that repo recognizes.
Those keys are hardcoded inside of the repo launcher itself -- look for the
`KEYRING_VERSION` and `MAINTAINER_KEYS` settings.
Adding new keys to the repo launcher will allow tags to be recognized by new
keys, but only people using that updated version will be able to.
Since the majority of users will be using an official launcher version, their
version will simply ignore any new signed tags.
If you want to add new keys, it's best to register them long ahead of time,
and then wait for that updated launcher to make its way out to everyone.
Even then, there will be a long tail of users with outdated launchers, so be
prepared for people asking questions.
### Registering a new key
The process of actually adding a new key is quite simple.
1. Add the public half of the key to `MAINTAINER_KEYS`.
2. Increment `KEYRING_VERSION` so repo knows it needs to update.
3. Wait a long time after that version is in a release (~months) before trying
to create a new release using those new keys.
## Self update algorithm
When creating a new repo checkout with `repo init`, there are a few options that
control how repo finds updates:
* `--repo-url`: This tells repo where to clone the full repo project itself.
It defaults to the official project (`REPO_URL` in the launcher script).
* `--repo-branch`: This tells repo which branch to use for the full project.
It defaults to the `stable` branch (`REPO_REV` in the launcher script).
Whenever `repo sync` is run, repo will check to see if an update is available.
It fetches the latest repo-branch from the repo-url.
Then it verifies that the latest commit in the branch has a valid signed tag
using `git tag -v` (which uses gpg).
If the tag is valid, then repo will update its internal checkout to it.
If the latest commit doesn't have a signed tag, repo will fall back to the
most recent tag it can find (via `git describe`).
If that tag is valid, then repo will warn and use that commit instead.
If that tag cannot be verified, it gives up and forces the user to resolve.
## Branch management
All development happens on the `master` branch and should generally be stable.
Since the repo launcher defaults to tracking the `stable` branch, it is not
normally updated until a new release is available.
If something goes wrong with a new release, an older release can be force pushed
and clients will automatically downgrade.
The `maint` branch is used to track the previous major release of repo.
It is not normally meant to be used by people as `stable` should be good enough.
Once a new major release is pushed to the `stable` branch, then the previous
major release can be pushed to `maint`.
For example, when `stable` moves from `v1.10.x` to `v1.11.x`, then the `maint`
branch will be updated from `v1.9.x` to `v1.10.x`.
We don't have parallel release branches/series.
Typically all tags are made against the `master` branch and then pushed to the
`stable` branch to make it available to the rest of the world.
Since repo doesn't typically see a lot of changes, this tends to be OK.
## Creating a new release
When you want to create a new release, you'll need to select a good version and
create a signed tag using a key registered in repo itself.
Typically we just tag the latest version of the `master` branch.
The tag could be pushed now, but it won't be used by clients normally (since the
default `repo-branch` setting is `stable`).
This would allow some early testing on systems who explicitly select `master`.
### Creating a signed tag
Lets assume your keys live in a dedicated directory, e.g. `~/.gnupg/repo/`.
*** note
If you need access to the official keys, check out the internal documentation
at [go/repo-release].
Note that only official maintainers of repo will have access as it describes
internal processes for accessing the restricted keys.
***
```sh
# Set the gpg key directory.
$ export GNUPGHOME=~/.gnupg/repo/
# Verify the listed key is “Repo Maintainer”.
$ gpg -K
# Pick whatever branch or commit you want to tag.
$ r=master
# Pick the new version.
$ t=1.12.10
# Create the signed tag.
$ git tag -s v$t -u "Repo Maintainer <repo@android.kernel.org>" -m "repo $t" $r
# Verify the signed tag.
$ git show v$t
```
### Push the new release
Once you're ready to make the release available to everyone, push it to the
`stable` branch.
Make sure you never push the tag itself to the stable branch!
Only push the commit -- notice the use of `$t` and `$r` below.
```sh
$ git push https://gerrit-review.googlesource.com/git-repo v$t
$ git push https://gerrit-review.googlesource.com/git-repo $r:stable
```
If something goes horribly wrong, you can force push the previous version to the
`stable` branch and people should automatically recover.
Again, make sure you never push the tag itself!
```sh
$ oldrev="whatever-old-commit"
$ git push https://gerrit-review.googlesource.com/git-repo $oldrev:stable --force
```
### Announce the release
Once you do push a new release to `stable`, make sure to announce it on the
[repo-discuss@googlegroups.com] group.
Here is an [example announcement].
You can create a short changelog using the command:
```sh
# If you haven't pushed to the stable branch yet, you can use origin/stable.
# If you have pushed, change origin/stable to the previous release tag.
$ 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
[repo-discuss@googlegroups.com]: https://groups.google.com/forum/#!forum/repo-discuss
[go/repo-release]: https://goto.google.com/repo-release

View File

@ -83,6 +83,31 @@ then check it directly. Hooks should not normally modify the active git repo
the user. Although user interaction is discouraged in the common case, it can
be useful when deploying automatic fixes.
### Shebang Handling
*** note
This is intended as a transitional feature. Hooks are expected to eventually
migrate to Python 3 only as Python 2 is EOL & deprecated.
***
If the hook is written against a specific version of Python (either 2 or 3),
the script can declare that explicitly. Repo will then attempt to execute it
under the right version of Python regardless of the version repo itself might
be executing under.
Here are the shebangs that are recognized.
* `#!/usr/bin/env python` & `#!/usr/bin/python`: The hook is compatible with
Python 2 & Python 3. For maximum compatibility, these are recommended.
* `#!/usr/bin/env python2` & `#!/usr/bin/python2`: The hook requires Python 2.
Version specific names like `python2.7` are also recognized.
* `#!/usr/bin/env python3` & `#!/usr/bin/python3`: The hook requires Python 3.
Version specific names like `python3.6` are also recognized.
If no shebang is detected, or does not match the forms above, we assume that the
hook is compatible with both Python 2 & Python 3 as if `#!/usr/bin/python` was
used.
## Hooks
Here are all the points available for hooking.

144
docs/windows.md Normal file
View File

@ -0,0 +1,144 @@
# Microsoft Windows Details
Repo is primarily developed on Linux with a lot of users on macOS.
Windows is, unfortunately, not a common platform.
There is support in repo for Windows, but there might be some rough edges.
Keep in mind that Windows in general is "best effort" and "community supported".
That means we don't actively test or verify behavior, but rely heavily on users
to report problems back to us, and to contribute fixes as needed.
[TOC]
## Windows
We only support Windows 10 or newer.
This is largely due to symlinks not being available in older versions, but it's
also due to most developers not using Windows.
We will never add code specific to older versions of Windows.
It might work, but it most likely won't, so please don't bother asking.
## Symlinks
Repo will use symlinks heavily internally.
On *NIX platforms, this isn't an issue, but Windows makes it a bit difficult.
There are some documents out there for how to do this, but usually the easiest
answer is to run your shell as an Administrator and invoke repo/git in that.
This isn't a great solution, but Windows doesn't make this easy, so here we are.
### Launch Git Bash
If you install Git Bash (see below), you can launch that with appropriate
permissions so that all programs "just work".
* Open the Start Menu (i.e. press the ⊞ key).
* Find/search for "Git Bash".
* Right click it and select "Run as administrator".
*** note
**NB**: This environment is only needed when running `repo`, or any specific `git`
command that might involve symlinks (e.g. `pull` or `checkout`).
You do not need to run all your commands in here such as your editor.
***
### Symlinks with GNU tools
If you want to use `ln -s` inside of the default Git/bash shell, you might need
to export this environment variable:
```sh
$ export MSYS="winsymlinks:nativestrict"
```
Otherwise `ln -s` will copy files and not actually create a symlink.
This also helps `tar` unpack symlinks, so that's nice.
### References
* https://github.com/git-for-windows/git/wiki/Symbolic-Links
* https://blogs.windows.com/windowsdeveloper/2016/12/02/symlinks-windows-10/
## Python
You should make sure to be running Python 3.6 or newer under Windows.
Python 2 might work, but due to already limited platform testing, you should
only run newer Python versions.
See our [Python Support](./python-support.md) document for more details.
You can grab the latest Windows installer here:<br>
https://www.python.org/downloads/release/python-3
## Git
You should install the most recent version of Git for Windows:<br>
https://git-scm.com/download/win
When installing, make sure to turn on "Enable symbolic links" when prompted.
If you've already installed Git for Windows, you can simply download the latest
installer from above and run it again.
It should safely upgrade things in situ for you.
This is useful if you want to switch the symbolic link option after the fact.
## Shell
We don't have a specific requirement for shell environments when running repo.
Most developers use MinTTY/bash that's included with the Git for Windows install
(so see above for installing Git).
Command & Powershell & the Windows Terminal probably work.
Who knows!
## FAQ
### repo upload always complains about allowing hooks or using --no-verify!
When using `repo upload` in projects that have custom repohooks, you might get
an error like the following:
```sh
$ repo upload
ERROR: You must allow the pre-upload hook or use --no-verify.
```
This can be confusing as you never get prompted.
[MinTTY has a bug][mintty] that breaks isatty checking inside of repo which
causes repo to never interactively prompt the user which means the upload check
always fails.
You can workaround this by manually granting consent when uploading.
Simply add the `--verify` option whenever uploading:
```sh
$ repo upload --verify
```
You will have to specify this flag every time you upload.
[mintty]: https://github.com/mintty/mintty/issues/56
### repohooks always fail with an close_fds error.
When using the [reference repohooks project][repohooks] included in AOSP,
you might see errors like this when running `repo upload`:
```sh
$ repo upload
ERROR: Traceback (most recent call last):
...
File "C:\...\lib\subprocess.py", line 351, in __init__
raise ValueError("close_fds is not supported on Windows "
ValueError: close_fds is not supported on Windows platforms if you redirect stdin/stderr/stdout
Failed to run main() for pre-upload hook; see traceback above.
```
This error shows up when using Python 2.
You should upgrade to Python 3 instead (see above).
If you already have Python 3 installed, make sure it's the default version.
Running `python --version` should say `Python 3`, not `Python 2`.
If you didn't install the Python versions, or don't have permission to change
the default version, you can probably workaround this by changing `$PATH` in
your shell so the Python 3 version is found first.
[repohooks]: https://android.googlesource.com/platform/tools/repohooks

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#
@ -67,11 +68,14 @@ least one of these before using this command.""", file=sys.stderr)
def EditString(cls, data):
"""Opens an editor to edit the given content.
Args:
data : the text to edit
Args:
data: The text to edit.
Returns:
new value of edited text; None if editing did not succeed
Returns:
New value of edited text.
Raises:
EditorError: The editor failed to run.
"""
editor = cls._GetEditor()
if editor == ':':
@ -79,7 +83,7 @@ least one of these before using this command.""", file=sys.stderr)
fd, path = tempfile.mkstemp()
try:
os.write(fd, data)
os.write(fd, data.encode('utf-8'))
os.close(fd)
fd = None
@ -105,11 +109,8 @@ least one of these before using this command.""", file=sys.stderr)
raise EditorError('editor failed with exit status %d: %s %s'
% (rc, editor, path))
fd2 = open(path)
try:
return fd2.read()
finally:
fd2.close()
with open(path, mode='rb') as fd2:
return fd2.read().decode('utf-8')
finally:
if fd:
os.close(fd)

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#
@ -21,6 +22,10 @@ class ManifestInvalidRevisionError(Exception):
"""The revision value in a project is incorrect.
"""
class ManifestInvalidPathError(Exception):
"""A path used in <copyfile> or <linkfile> is incorrect.
"""
class NoManifestException(Exception):
"""The required manifest does not exist.
"""

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2017 The Android Open Source Project
#
@ -18,8 +19,6 @@ from __future__ import print_function
import json
import multiprocessing
from pyversion import is_python3
TASK_COMMAND = 'command'
TASK_SYNC_NETWORK = 'sync-network'
TASK_SYNC_LOCAL = 'sync-local'
@ -53,7 +52,6 @@ class EventLog(object):
def __init__(self):
"""Initializes the event log."""
self._log = []
self._next_id = _EventIdGenerator()
self._parent = None
def Add(self, name, task_name, start, finish=None, success=None,
@ -73,7 +71,7 @@ class EventLog(object):
A dictionary of the event added to the log.
"""
event = {
'id': (kind, self._next_id.__next__() if is_python3() else self._next_id.next()),
'id': (kind, _NextEventId()),
'name': name,
'task_name': task_name,
'start_time': start,
@ -164,16 +162,16 @@ class EventLog(object):
f.write('\n')
def _EventIdGenerator():
"""Returns multi-process safe iterator that generates locally unique id.
# An integer id that is unique across this invocation of the program.
_EVENT_ID = multiprocessing.Value('i', 1)
Yields:
def _NextEventId():
"""Helper function for grabbing the next unique id.
Returns:
A unique, to this invocation of the program, integer id.
"""
eid = multiprocessing.Value('i', 1)
while True:
with eid.get_lock():
val = eid.value
eid.value += 1
yield val
with _EVENT_ID.get_lock():
val = _EVENT_ID.value
_EVENT_ID.value += 1
return val

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#
@ -21,12 +22,23 @@ import tempfile
from signal import SIGTERM
from error import GitError
from git_refs import HEAD
import platform_utils
from trace import REPO_TRACE, IsTrace, Trace
from repo_trace import REPO_TRACE, IsTrace, Trace
from wrapper import Wrapper
GIT = 'git'
MIN_GIT_VERSION = (1, 5, 4)
# NB: These do not need to be kept in sync with the repo launcher script.
# 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'
LAST_GITDIR = None
@ -79,22 +91,12 @@ def terminate_ssh_clients():
_git_version = None
class _GitCall(object):
def version(self):
p = GitCommand(None, ['--version'], capture_stdout=True)
if p.Wait() == 0:
if hasattr(p.stdout, 'decode'):
return p.stdout.decode('utf-8')
else:
return p.stdout
return None
def version_tuple(self):
global _git_version
if _git_version is None:
ver_str = git.version()
_git_version = Wrapper().ParseGitVersion(ver_str)
_git_version = Wrapper().ParseGitVersion()
if _git_version is None:
print('fatal: "%s" unsupported' % ver_str, file=sys.stderr)
print('fatal: unable to detect git version', file=sys.stderr)
sys.exit(1)
return _git_version
@ -107,13 +109,95 @@ class _GitCall(object):
return fun
git = _GitCall()
def git_require(min_version, fail=False):
def RepoSourceVersion():
"""Return the version of the repo.git tree."""
ver = getattr(RepoSourceVersion, 'version', None)
# We avoid GitCommand so we don't run into circular deps -- GitCommand needs
# to initialize version info we provide.
if ver is None:
env = GitCommand._GetBasicEnv()
proj = os.path.dirname(os.path.abspath(__file__))
env[GIT_DIR] = os.path.join(proj, '.git')
p = subprocess.Popen([GIT, 'describe', HEAD], stdout=subprocess.PIPE,
env=env)
if p.wait() == 0:
ver = p.stdout.read().strip().decode('utf-8')
if ver.startswith('v'):
ver = ver[1:]
else:
ver = 'unknown'
setattr(RepoSourceVersion, 'version', ver)
return ver
class UserAgent(object):
"""Mange User-Agent settings when talking to external services
We follow the style as documented here:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
"""
_os = None
_repo_ua = None
_git_ua = None
@property
def os(self):
"""The operating system name."""
if self._os is None:
os_name = sys.platform
if os_name.lower().startswith('linux'):
os_name = 'Linux'
elif os_name == 'win32':
os_name = 'Win32'
elif os_name == 'cygwin':
os_name = 'Cygwin'
elif os_name == 'darwin':
os_name = 'Darwin'
self._os = os_name
return self._os
@property
def repo(self):
"""The UA when connecting directly from repo."""
if self._repo_ua is None:
py_version = sys.version_info
self._repo_ua = 'git-repo/%s (%s) git/%s Python/%d.%d.%d' % (
RepoSourceVersion(),
self.os,
git.version_tuple().full,
py_version.major, py_version.minor, py_version.micro)
return self._repo_ua
@property
def git(self):
"""The UA when running git."""
if self._git_ua is None:
self._git_ua = 'git/%s (%s) git-repo/%s' % (
git.version_tuple().full,
self.os,
RepoSourceVersion())
return self._git_ua
user_agent = UserAgent()
def git_require(min_version, fail=False, msg=''):
git_version = git.version_tuple()
if min_version <= git_version:
return True
if fail:
need = '.'.join(map(str, min_version))
print('fatal: git %s or later required' % need, file=sys.stderr)
if msg:
msg = ' for ' + msg
print('fatal: git %s or later required%s' % (need, msg), file=sys.stderr)
sys.exit(1)
return False
@ -132,17 +216,7 @@ class GitCommand(object):
ssh_proxy = False,
cwd = None,
gitdir = None):
env = os.environ.copy()
for key in [REPO_TRACE,
GIT_DIR,
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
'GIT_OBJECT_DIRECTORY',
'GIT_WORK_TREE',
'GIT_GRAFT_FILE',
'GIT_INDEX_FILE']:
if key in env:
del env[key]
env = self._GetBasicEnv()
# If we are not capturing std* then need to print it.
self.tee = {'stdout': not capture_stdout, 'stderr': not capture_stderr}
@ -162,6 +236,7 @@ class GitCommand(object):
if 'GIT_ALLOW_PROTOCOL' not in env:
_setenv(env, 'GIT_ALLOW_PROTOCOL',
'file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc')
_setenv(env, 'GIT_HTTP_USER_AGENT', user_agent.git)
if project:
if not cwd:
@ -234,6 +309,23 @@ class GitCommand(object):
self.process = p
self.stdin = p.stdin
@staticmethod
def _GetBasicEnv():
"""Return a basic env for running git under.
This is guaranteed to be side-effect free.
"""
env = os.environ.copy()
for key in (REPO_TRACE,
GIT_DIR,
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
'GIT_OBJECT_DIRECTORY',
'GIT_WORK_TREE',
'GIT_GRAFT_FILE',
'GIT_INDEX_FILE'):
env.pop(key, None)
return env
def Wait(self):
try:
p = self.process

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#
@ -43,7 +44,7 @@ else:
from signal import SIGTERM
from error import GitError, UploadError
import platform_utils
from trace import Trace
from repo_trace import Trace
if is_python3():
from http.client import HTTPException
else:
@ -275,22 +276,16 @@ class GitConfig(object):
return None
try:
Trace(': parsing %s', self.file)
fd = open(self._json)
try:
with open(self._json) as fd:
return json.load(fd)
finally:
fd.close()
except (IOError, ValueError):
platform_utils.remove(self._json)
return None
def _SaveJson(self, cache):
try:
fd = open(self._json, 'w')
try:
with open(self._json, 'w') as fd:
json.dump(cache, fd, indent=2)
finally:
fd.close()
except (IOError, TypeError):
if os.path.exists(self._json):
platform_utils.remove(self._json)
@ -533,7 +528,7 @@ def GetUrlCookieFile(url, quiet):
cookiefile = None
proxy = None
for line in p.stdout:
line = line.strip()
line = line.strip().decode('utf-8')
if line.startswith(cookieprefix):
cookiefile = os.path.expanduser(line[len(cookieprefix):])
if line.startswith(proxyprefix):
@ -545,7 +540,7 @@ def GetUrlCookieFile(url, quiet):
finally:
p.stdin.close()
if p.wait():
err_msg = p.stderr.read()
err_msg = p.stderr.read().decode('utf-8')
if ' -print_config' in err_msg:
pass # Persistent proxy doesn't support -print_config.
elif not quiet:
@ -656,13 +651,14 @@ class Remote(object):
info = urllib.request.urlopen(info_url, context=context).read()
else:
info = urllib.request.urlopen(info_url).read()
if info == 'NOT_AVAILABLE' or '<' in info:
if info == b'NOT_AVAILABLE' or b'<' in info:
# If `info` contains '<', we assume the server gave us some sort
# of HTML response back, like maybe a login page.
#
# Assume HTTP if SSH is not enabled or ssh_info doesn't look right.
self._review_url = http_url
else:
info = info.decode('utf-8')
host, port = info.split()
self._review_url = self._SshReviewUrl(userEmail, host, port)
except urllib.error.HTTPError as e:
@ -697,7 +693,8 @@ class Remote(object):
if not rev.startswith(R_HEADS):
return rev
raise GitError('remote %s does not have %s' % (self.name, rev))
raise GitError('%s: remote %s does not have %s' %
(self.projectname, self.name, rev))
def WritesTo(self, ref):
"""True if the remote stores to the tracking ref.
@ -770,15 +767,12 @@ class Branch(object):
self._Set('merge', self.merge)
else:
fd = open(self._config.file, 'a')
try:
with open(self._config.file, 'a') as fd:
fd.write('[branch "%s"]\n' % self.name)
if self.remote:
fd.write('\tremote = %s\n' % self.remote.name)
if self.merge:
fd.write('\tmerge = %s\n' % self.merge)
finally:
fd.close()
def _Set(self, key, value):
key = 'branch.%s.%s' % (self.name, key)

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2009 The Android Open Source Project
#
@ -14,7 +15,7 @@
# limitations under the License.
import os
from trace import Trace
from repo_trace import Trace
import platform_utils
HEAD = 'HEAD'
@ -140,18 +141,11 @@ class GitRefs(object):
def _ReadLoose1(self, path, name):
try:
fd = open(path)
except IOError:
return
try:
try:
with open(path) as fd:
mtime = os.path.getmtime(path)
ref_id = fd.readline()
except (IOError, OSError):
return
finally:
fd.close()
except (IOError, OSError):
return
try:
ref_id = ref_id.decode()

15
git_ssh
View File

@ -1,2 +1,17 @@
#!/bin/sh
#
# Copyright (C) 2009 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
exec ssh -o "ControlMaster no" -o "ControlPath $REPO_SSH_SOCK" "$@"

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2015 The Android Open Source Project
#
@ -58,8 +59,8 @@ def _set_project_revisions(projects):
sys.exit(1)
revisionExpr = gitcmd.stdout.split('\t')[0]
if not revisionExpr:
raise(ManifestParseError('Invalid SHA-1 revision project %s (%s)' %
(proj.remote.url, proj.revisionExpr)))
raise ManifestParseError('Invalid SHA-1 revision project %s (%s)' %
(proj.remote.url, proj.revisionExpr))
proj.revisionExpr = revisionExpr
def _manifest_groups(manifest):
@ -87,7 +88,7 @@ def generate_gitc_manifest(gitc_manifest, manifest, paths=None):
print('Generating GITC Manifest by fetching revision SHAs for each '
'project.')
if paths is None:
paths = manifest.paths.keys()
paths = list(manifest.paths.keys())
groups = [x for x in re.split(r'[,\s]+', _manifest_groups(manifest)) if x]
@ -96,7 +97,7 @@ def generate_gitc_manifest(gitc_manifest, manifest, paths=None):
projects = [p for p in projects if p.MatchesGroups(groups)]
if gitc_manifest is not None:
for path, proj in manifest.paths.iteritems():
for path, proj in manifest.paths.items():
if not proj.MatchesGroups(groups):
continue
@ -124,7 +125,7 @@ def generate_gitc_manifest(gitc_manifest, manifest, paths=None):
index += NUM_BATCH_RETRIEVE_REVISIONID
if gitc_manifest is not None:
for path, proj in gitc_manifest.paths.iteritems():
for path, proj in gitc_manifest.paths.items():
if proj.old_revision and path in paths:
# If we updated a project that has been started, keep the old-revision
# updated.
@ -133,7 +134,7 @@ def generate_gitc_manifest(gitc_manifest, manifest, paths=None):
repo_proj.revisionExpr = None
# Convert URLs from relative to absolute.
for _name, remote in manifest.remotes.iteritems():
for _name, remote in manifest.remotes.items():
remote.fetchUrl = remote.resolvedFetchUrl
# Save the manifest.

128
main.py
View File

@ -1,4 +1,5 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#
@ -14,19 +15,26 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""The repo tool.
People shouldn't run this directly; instead, they should use the `repo` wrapper
which takes care of execing this entry point.
"""
from __future__ import print_function
import getpass
import imp
import netrc
import optparse
import os
import sys
import textwrap
import time
from pyversion import is_python3
if is_python3():
import urllib.request
else:
import imp
import urllib2
urllib = imp.new_module('urllib')
urllib.request = urllib2
@ -38,8 +46,8 @@ except ImportError:
from color import SetDefaultColoring
import event_log
from trace import SetTrace
from git_command import git, GitCommand
from repo_trace import SetTrace
from git_command import git, GitCommand, user_agent
from git_config import init_ssh, close_ssh
from command import InteractiveCommand
from command import MirrorSafeCommand
@ -64,8 +72,10 @@ if not is_python3():
input = raw_input
global_options = optparse.OptionParser(
usage="repo [-p|--paginate|--no-pager] COMMAND [ARGS]"
)
usage='repo [-p|--paginate|--no-pager] COMMAND [ARGS]',
add_help_option=False)
global_options.add_option('-h', '--help', action='store_true',
help='show this help message and exit')
global_options.add_option('-p', '--paginate',
dest='pager', action='store_true',
help='display command output in the pager')
@ -77,7 +87,10 @@ global_options.add_option('--color',
help='control color usage: auto, always, never')
global_options.add_option('--trace',
dest='trace', action='store_true',
help='trace git command execution')
help='trace git command execution (REPO_TRACE=1)')
global_options.add_option('--trace-python',
dest='trace_python', action='store_true',
help='trace python command execution')
global_options.add_option('--time',
dest='time', action='store_true',
help='time repo command execution')
@ -95,8 +108,8 @@ class _Repo(object):
# add 'branch' as an alias for 'branches'
all_commands['branch'] = all_commands['branches']
def _Run(self, argv):
result = 0
def _ParseArgs(self, argv):
"""Parse the main `repo` command line options."""
name = None
glob = []
@ -113,6 +126,20 @@ class _Repo(object):
argv = []
gopts, _gargs = global_options.parse_args(glob)
if gopts.help:
global_options.print_help()
commands = ' '.join(sorted(self.commands))
wrapped_commands = textwrap.wrap(commands, width=77)
print('\nAvailable commands:\n %s' % ('\n '.join(wrapped_commands),))
print('\nRun `repo help <command>` for command-specific details.')
global_options.exit()
return (name, gopts, argv)
def _Run(self, name, gopts, argv):
"""Execute the requested subcommand."""
result = 0
if gopts.trace:
SetTrace()
if gopts.show_version:
@ -181,6 +208,7 @@ class _Repo(object):
cmd_event = cmd.event_log.Add(name, event_log.TASK_COMMAND, start)
cmd.event_log.SetParent(cmd_event)
try:
cmd.ValidateOptions(copts, cargs)
result = cmd.Execute(copts, cargs)
except (DownloadError, ManifestInvalidRevisionError,
NoManifestException) as e:
@ -227,31 +255,41 @@ class _Repo(object):
return result
def _MyRepoPath():
return os.path.dirname(__file__)
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)
def _CheckWrapperVersion(ver, repo_path):
if not repo_path:
repo_path = '~/bin/repo'
if not ver:
if not ver_str:
print('no --wrapper-version argument', file=sys.stderr)
sys.exit(1)
# Pull out the version of the repo launcher we know about to compare.
exp = Wrapper().VERSION
ver = tuple(map(int, ver.split('.')))
if len(ver) == 1:
ver = (0, ver[0])
ver = tuple(map(int, ver_str.split('.')))
exp_str = '.'.join(map(str, exp))
if exp[0] > ver[0] or ver < (0, 4):
if ver < MIN_REPO_VERSION:
print("""
!!! A new repo command (%5s) is available. !!!
!!! You must upgrade before you can continue: !!!
repo: error:
!!! 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
""" % (exp_str, WrapperPath(), repo_path), file=sys.stderr)
""" % (ver_str, min_str, exp_str, WrapperPath(), repo_path), file=sys.stderr)
sys.exit(1)
if exp > ver:
@ -282,51 +320,13 @@ def _PruneOptions(argv, opt):
continue
i += 1
_user_agent = None
def _UserAgent():
global _user_agent
if _user_agent is None:
py_version = sys.version_info
os_name = sys.platform
if os_name == 'linux2':
os_name = 'Linux'
elif os_name == 'win32':
os_name = 'Win32'
elif os_name == 'cygwin':
os_name = 'Cygwin'
elif os_name == 'darwin':
os_name = 'Darwin'
p = GitCommand(
None, ['describe', 'HEAD'],
cwd = _MyRepoPath(),
capture_stdout = True)
if p.Wait() == 0:
repo_version = p.stdout
if len(repo_version) > 0 and repo_version[-1] == '\n':
repo_version = repo_version[0:-1]
if len(repo_version) > 0 and repo_version[0] == 'v':
repo_version = repo_version[1:]
else:
repo_version = 'unknown'
_user_agent = 'git-repo/%s (%s) git/%s Python/%d.%d.%d' % (
repo_version,
os_name,
'.'.join(map(str, git.version_tuple())),
py_version[0], py_version[1], py_version[2])
return _user_agent
class _UserAgentHandler(urllib.request.BaseHandler):
def http_request(self, req):
req.add_header('User-Agent', _UserAgent())
req.add_header('User-Agent', user_agent.repo)
return req
def https_request(self, req):
req.add_header('User-Agent', _UserAgent())
req.add_header('User-Agent', user_agent.repo)
return req
def _AddPasswordFromUserInput(handler, msg, req):
@ -519,7 +519,15 @@ def _Main(argv):
try:
init_ssh()
init_http()
result = repo._Run(argv) or 0
name, gopts, argv = repo._ParseArgs(argv)
run = lambda: repo._Run(name, gopts, argv) or 0
if gopts.trace_python:
import trace
tracer = trace.Trace(count=False, trace=True, timing=True,
ignoredirs=set(sys.path[1:]))
result = tracer.runfunc(run)
else:
result = run()
finally:
close_ssh()
except KeyboardInterrupt:

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#
@ -34,7 +35,8 @@ from git_config import GitConfig
from git_refs import R_HEADS, HEAD
import platform_utils
from project import RemoteSpec, Project, MetaProject
from error import ManifestParseError, ManifestInvalidRevisionError
from error import (ManifestParseError, ManifestInvalidPathError,
ManifestInvalidRevisionError)
MANIFEST_FILE_NAME = 'manifest.xml'
LOCAL_MANIFEST_NAME = 'local_manifest.xml'
@ -135,6 +137,7 @@ class XmlManifest(object):
self.globalConfig = GitConfig.ForUser()
self.localManifestWarning = False
self.isGitcClient = False
self._load_local_manifests = True
self.repoProject = MetaProject(self, 'repo',
gitdir = os.path.join(repodir, 'repo/.git'),
@ -146,15 +149,26 @@ class XmlManifest(object):
self._Unload()
def Override(self, name):
def Override(self, name, load_local_manifests=True):
"""Use a different manifest, just for the current instantiation.
"""
path = os.path.join(self.manifestProject.worktree, name)
if not os.path.isfile(path):
raise ManifestParseError('manifest %s not found' % name)
path = None
# Look for a manifest by path in the filesystem (including the cwd).
if not load_local_manifests:
local_path = os.path.abspath(name)
if os.path.isfile(local_path):
path = local_path
# Look for manifests by name from the manifests repo.
if path is None:
path = os.path.join(self.manifestProject.worktree, name)
if not os.path.isfile(path):
raise ManifestParseError('manifest %s not found' % name)
old = self.manifestFile
try:
self._load_local_manifests = load_local_manifests
self.manifestFile = path
self._Unload()
self._Load()
@ -400,6 +414,12 @@ class XmlManifest(object):
self._Load()
return self._manifest_server
@property
def CloneFilter(self):
if self.manifestProject.config.GetBoolean('repo.partialclone'):
return self.manifestProject.config.GetString('repo.clonefilter')
return None
@property
def IsMirror(self):
return self.manifestProject.config.GetBoolean('repo.mirror')
@ -435,23 +455,26 @@ class XmlManifest(object):
nodes.append(self._ParseManifestXml(self.manifestFile,
self.manifestProject.worktree))
local = os.path.join(self.repodir, LOCAL_MANIFEST_NAME)
if os.path.exists(local):
if not self.localManifestWarning:
self.localManifestWarning = True
print('warning: %s is deprecated; put local manifests in `%s` instead'
% (LOCAL_MANIFEST_NAME, os.path.join(self.repodir, LOCAL_MANIFESTS_DIR_NAME)),
file=sys.stderr)
nodes.append(self._ParseManifestXml(local, self.repodir))
if self._load_local_manifests:
local = os.path.join(self.repodir, LOCAL_MANIFEST_NAME)
if os.path.exists(local):
if not self.localManifestWarning:
self.localManifestWarning = True
print('warning: %s is deprecated; put local manifests '
'in `%s` instead' % (LOCAL_MANIFEST_NAME,
os.path.join(self.repodir, LOCAL_MANIFESTS_DIR_NAME)),
file=sys.stderr)
nodes.append(self._ParseManifestXml(local, self.repodir))
local_dir = os.path.abspath(os.path.join(self.repodir, LOCAL_MANIFESTS_DIR_NAME))
try:
for local_file in sorted(platform_utils.listdir(local_dir)):
if local_file.endswith('.xml'):
local = os.path.join(local_dir, local_file)
nodes.append(self._ParseManifestXml(local, self.repodir))
except OSError:
pass
local_dir = os.path.abspath(os.path.join(self.repodir,
LOCAL_MANIFESTS_DIR_NAME))
try:
for local_file in sorted(platform_utils.listdir(local_dir)):
if local_file.endswith('.xml'):
local = os.path.join(local_dir, local_file)
nodes.append(self._ParseManifestXml(local, self.repodir))
except OSError:
pass
try:
self._ParseManifest(nodes)
@ -498,7 +521,7 @@ class XmlManifest(object):
raise
except Exception as e:
raise ManifestParseError(
"failed parsing included manifest %s: %s", (name, e))
"failed parsing included manifest %s: %s" % (name, e))
else:
nodes.append(node)
return nodes
@ -576,6 +599,9 @@ class XmlManifest(object):
if groups:
groups = self._ParseGroups(groups)
revision = node.getAttribute('revision')
remote = node.getAttribute('remote')
if remote:
remote = self._get_remote(node)
for p in self._projects[name]:
if path and p.relpath != path:
@ -584,6 +610,8 @@ class XmlManifest(object):
p.groups.extend(groups)
if revision:
p.revisionExpr = revision
if remote:
p.remote = remote.ToRemoteSpec(name)
if node.nodeName == 'repo-hooks':
# Get the name of the project and the (space-separated) list of enabled.
repo_hooks_project = self._reqatt(node, 'in-project')
@ -921,21 +949,101 @@ class XmlManifest(object):
worktree = os.path.join(parent.worktree, path).replace('\\', '/')
return relpath, worktree, gitdir, objdir
@staticmethod
def _CheckLocalPath(path, symlink=False):
"""Verify |path| is reasonable for use in <copyfile> & <linkfile>."""
if '~' in path:
return '~ not allowed (due to 8.3 filenames on Windows filesystems)'
# Some filesystems (like Apple's HFS+) try to normalize Unicode codepoints
# which means there are alternative names for ".git". Reject paths with
# these in it as there shouldn't be any reasonable need for them here.
# The set of codepoints here was cribbed from jgit's implementation:
# https://eclipse.googlesource.com/jgit/jgit/+/9110037e3e9461ff4dac22fee84ef3694ed57648/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java#884
BAD_CODEPOINTS = {
u'\u200C', # ZERO WIDTH NON-JOINER
u'\u200D', # ZERO WIDTH JOINER
u'\u200E', # LEFT-TO-RIGHT MARK
u'\u200F', # RIGHT-TO-LEFT MARK
u'\u202A', # LEFT-TO-RIGHT EMBEDDING
u'\u202B', # RIGHT-TO-LEFT EMBEDDING
u'\u202C', # POP DIRECTIONAL FORMATTING
u'\u202D', # LEFT-TO-RIGHT OVERRIDE
u'\u202E', # RIGHT-TO-LEFT OVERRIDE
u'\u206A', # INHIBIT SYMMETRIC SWAPPING
u'\u206B', # ACTIVATE SYMMETRIC SWAPPING
u'\u206C', # INHIBIT ARABIC FORM SHAPING
u'\u206D', # ACTIVATE ARABIC FORM SHAPING
u'\u206E', # NATIONAL DIGIT SHAPES
u'\u206F', # NOMINAL DIGIT SHAPES
u'\uFEFF', # ZERO WIDTH NO-BREAK SPACE
}
if BAD_CODEPOINTS & set(path):
# This message is more expansive than reality, but should be fine.
return 'Unicode combining characters not allowed'
# Assume paths might be used on case-insensitive filesystems.
path = path.lower()
# Some people use src="." to create stable links to projects. Lets allow
# that but reject all other uses of "." to keep things simple.
parts = path.split(os.path.sep)
if parts != ['.']:
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):
return 'dirs not allowed'
norm = os.path.normpath(path)
if norm == '..' or norm.startswith('../') or norm.startswith(os.path.sep):
return 'path cannot be outside'
@classmethod
def _ValidateFilePaths(cls, element, src, dest):
"""Verify |src| & |dest| are reasonable for <copyfile> & <linkfile>.
We verify the path independent of any filesystem state as we won't have a
checkout available to compare to. i.e. This is for parsing validation
purposes only.
We'll do full/live sanity checking before we do the actual filesystem
modifications in _CopyFile/_LinkFile/etc...
"""
# |dest| is the file we write to or symlink we create.
# It is relative to the top of the repo client checkout.
msg = cls._CheckLocalPath(dest)
if msg:
raise ManifestInvalidPathError(
'<%s> invalid "dest": %s: %s' % (element, dest, msg))
# |src| is the file we read from or path we point to for symlinks.
# It is relative to the top of the git project checkout.
msg = cls._CheckLocalPath(src, symlink=element == 'linkfile')
if msg:
raise ManifestInvalidPathError(
'<%s> invalid "src": %s: %s' % (element, src, msg))
def _ParseCopyFile(self, project, node):
src = self._reqatt(node, 'src')
dest = self._reqatt(node, 'dest')
if not self.IsMirror:
# src is project relative;
# dest is relative to the top of the tree
project.AddCopyFile(src, dest, os.path.join(self.topdir, dest))
# dest is relative to the top of the tree.
# We only validate paths if we actually plan to process them.
self._ValidateFilePaths('copyfile', src, dest)
project.AddCopyFile(src, dest, self.topdir)
def _ParseLinkFile(self, project, node):
src = self._reqatt(node, 'src')
dest = self._reqatt(node, 'dest')
if not self.IsMirror:
# src is project relative;
# dest is relative to the top of the tree
project.AddLinkFile(src, dest, os.path.join(self.topdir, dest))
# dest is relative to the top of the tree.
# We only validate paths if we actually plan to process them.
self._ValidateFilePaths('linkfile', src, dest)
project.AddLinkFile(src, dest, self.topdir)
def _ParseAnnotation(self, project, node):
name = self._reqatt(node, 'name')

1
pager.py Executable file → Normal file
View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2016 The Android Open Source Project
#
@ -79,7 +80,7 @@ class FileDescriptorStreams(object):
"""
raise NotImplementedError
def _create_stream(fd, dest, std_name):
def _create_stream(self, fd, dest, std_name):
""" Creates a new stream wrapping an existing file descriptor.
"""
raise NotImplementedError
@ -240,14 +241,15 @@ def _makelongpath(path):
return path
def rmtree(path):
def rmtree(path, ignore_errors=False):
"""shutil.rmtree(path) wrapper with support for long paths on Windows.
Availability: Unix, Windows."""
onerror = None
if isWindows():
shutil.rmtree(_makelongpath(path), onerror=handle_rmtree_error)
else:
shutil.rmtree(path)
path = _makelongpath(path)
onerror = handle_rmtree_error
shutil.rmtree(path, ignore_errors=ignore_errors, onerror=onerror)
def handle_rmtree_error(function, path, excinfo):

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2016 The Android Open Source Project
#
@ -15,15 +16,21 @@
import errno
from pyversion import is_python3
from ctypes import WinDLL, get_last_error, FormatError, WinError, addressof
from ctypes import c_buffer
from ctypes.wintypes import BOOL, LPCWSTR, DWORD, HANDLE, POINTER, c_ubyte
from ctypes.wintypes import WCHAR, USHORT, LPVOID, Structure, Union, ULONG
from ctypes.wintypes import byref
from ctypes.wintypes import BOOL, BOOLEAN, LPCWSTR, DWORD, HANDLE
from ctypes.wintypes import WCHAR, USHORT, LPVOID, ULONG
if is_python3():
from ctypes import c_ubyte, Structure, Union, byref
from ctypes.wintypes import LPDWORD
else:
# For legacy Python2 different imports are needed.
from ctypes.wintypes import POINTER, c_ubyte, Structure, Union, byref
LPDWORD = POINTER(DWORD)
kernel32 = WinDLL('kernel32', use_last_error=True)
LPDWORD = POINTER(DWORD)
UCHAR = c_ubyte
# Win32 error codes
@ -33,7 +40,7 @@ ERROR_PRIVILEGE_NOT_HELD = 1314
# Win32 API entry points
CreateSymbolicLinkW = kernel32.CreateSymbolicLinkW
CreateSymbolicLinkW.restype = BOOL
CreateSymbolicLinkW.restype = BOOLEAN
CreateSymbolicLinkW.argtypes = (LPCWSTR, # lpSymlinkFileName In
LPCWSTR, # lpTargetFileName In
DWORD) # dwFlags In
@ -145,19 +152,12 @@ def create_dirsymlink(source, link_name):
def _create_symlink(source, link_name, dwFlags):
# Note: Win32 documentation for CreateSymbolicLink is incorrect.
# On success, the function returns "1".
# On error, the function returns some random value (e.g. 1280).
# The best bet seems to use "GetLastError" and check for error/success.
CreateSymbolicLinkW(link_name, source, dwFlags | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE)
code = get_last_error()
if code != ERROR_SUCCESS:
if not CreateSymbolicLinkW(link_name, source, dwFlags | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE):
# See https://github.com/golang/go/pull/24307/files#diff-b87bc12e4da2497308f9ef746086e4f0
# "the unprivileged create flag is unsupported below Windows 10 (1703, v10.0.14972).
# retry without it."
CreateSymbolicLinkW(link_name, source, dwFlags)
code = get_last_error()
if code != ERROR_SUCCESS:
if not CreateSymbolicLinkW(link_name, source, dwFlags):
code = get_last_error()
error_desc = FormatError(code).strip()
if code == ERROR_PRIVILEGE_NOT_HELD:
raise OSError(errno.EPERM, error_desc, link_name)
@ -185,7 +185,7 @@ def readlink(path):
if reparse_point_handle == INVALID_HANDLE_VALUE:
_raise_winerror(
get_last_error(),
'Error opening symblic link \"%s\"'.format(path))
'Error opening symbolic link \"%s\"'.format(path))
target_buffer = c_buffer(MAXIMUM_REPARSE_DATA_BUFFER_SIZE)
n_bytes_returned = DWORD()
io_result = DeviceIoControl(reparse_point_handle,
@ -200,7 +200,7 @@ def readlink(path):
if not io_result:
_raise_winerror(
get_last_error(),
'Error reading symblic link \"%s\"'.format(path))
'Error reading symbolic link \"%s\"'.format(path))
rdb = REPARSE_DATA_BUFFER.from_buffer(target_buffer)
if rdb.ReparseTag == IO_REPARSE_TAG_SYMLINK:
return _preserve_encoding(path, rdb.SymbolicLinkReparseBuffer.PrintName)
@ -209,11 +209,15 @@ def readlink(path):
# Unsupported reparse point type
_raise_winerror(
ERROR_NOT_SUPPORTED,
'Error reading symblic link \"%s\"'.format(path))
'Error reading symbolic link \"%s\"'.format(path))
def _preserve_encoding(source, target):
"""Ensures target is the same string type (i.e. unicode or str) as source."""
if is_python3():
return target
if isinstance(source, unicode):
return unicode(target)
return str(target)

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2009 The Android Open Source Project
#
@ -16,10 +17,15 @@
import os
import sys
from time import time
from trace import IsTrace
from repo_trace import IsTrace
_NOT_TTY = not os.isatty(2)
# This will erase all content in the current line (wherever the cursor is).
# It does not move the cursor, so this is usually followed by \r to move to
# column 0.
CSI_ERASE_LINE = '\x1b[2K'
class Progress(object):
def __init__(self, title, total=0, units='', print_newline=False,
always_print_percentage=False):
@ -33,7 +39,7 @@ class Progress(object):
self._print_newline = print_newline
self._always_print_percentage = always_print_percentage
def update(self, inc=1):
def update(self, inc=1, msg=''):
self._done += inc
if _NOT_TTY or IsTrace():
@ -46,7 +52,8 @@ class Progress(object):
return
if self._total <= 0:
sys.stderr.write('\r%s: %d, ' % (
sys.stderr.write('%s\r%s: %d,' % (
CSI_ERASE_LINE,
self._title,
self._done))
sys.stderr.flush()
@ -55,11 +62,13 @@ class Progress(object):
if self._lastp != p or self._always_print_percentage:
self._lastp = p
sys.stderr.write('\r%s: %3d%% (%d%s/%d%s)%s' % (
sys.stderr.write('%s\r%s: %3d%% (%d%s/%d%s)%s%s%s' % (
CSI_ERASE_LINE,
self._title,
p,
self._done, self._units,
self._total, self._units,
' ' if msg else '', msg,
"\n" if self._print_newline else ""))
sys.stderr.flush()
@ -68,13 +77,15 @@ class Progress(object):
return
if self._total <= 0:
sys.stderr.write('\r%s: %d, done. \n' % (
sys.stderr.write('%s\r%s: %d, done.\n' % (
CSI_ERASE_LINE,
self._title,
self._done))
sys.stderr.flush()
else:
p = (100 * self._done) / self._total
sys.stderr.write('\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,
self._title,
p,
self._done, self._units,

639
project.py Executable file → Normal file
View File

@ -1,3 +1,5 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
@ -16,6 +18,7 @@ from __future__ import print_function
import errno
import filecmp
import glob
import json
import os
import random
import re
@ -33,10 +36,11 @@ from git_command import GitCommand, git_require
from git_config import GitConfig, IsId, GetSchemeFromUrl, GetUrlCookieFile, \
ID_RE
from error import GitError, HookError, UploadError, DownloadError
from error import ManifestInvalidRevisionError
from error import ManifestInvalidRevisionError, ManifestInvalidPathError
from error import NoManifestException
import platform_utils
from trace import IsTrace, Trace
import progress
from repo_trace import IsTrace, Trace
from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M
@ -54,11 +58,8 @@ else:
def _lwrite(path, content):
lock = '%s.lock' % path
fd = open(lock, 'w')
try:
with open(lock, 'w') as fd:
fd.write(content)
finally:
fd.close()
try:
platform_utils.rename(lock, path)
@ -133,6 +134,7 @@ class DownloadedChange(object):
class ReviewableBranch(object):
_commit_cache = None
_base_exists = None
def __init__(self, project, branch, base):
self.project = project
@ -146,14 +148,19 @@ class ReviewableBranch(object):
@property
def commits(self):
if self._commit_cache is None:
self._commit_cache = self.project.bare_git.rev_list('--abbrev=8',
'--abbrev-commit',
'--pretty=oneline',
'--reverse',
'--date-order',
not_rev(self.base),
R_HEADS + self.name,
'--')
args = ('--abbrev=8', '--abbrev-commit', '--pretty=oneline', '--reverse',
'--date-order', not_rev(self.base), R_HEADS + self.name, '--')
try:
self._commit_cache = self.project.bare_git.rev_list(*args)
except GitError:
# We weren't able to probe the commits for this branch. Was it tracking
# a branch that no longer exists? If so, return no commits. Otherwise,
# rethrow the error as we don't know what's going on.
if self.base_exists:
raise
self._commit_cache = []
return self._commit_cache
@property
@ -172,6 +179,23 @@ class ReviewableBranch(object):
R_HEADS + self.name,
'--')
@property
def base_exists(self):
"""Whether the branch we're tracking exists.
Normally it should, but sometimes branches we track can get deleted.
"""
if self._base_exists is None:
try:
self.project.bare_git.rev_parse('--verify', not_rev(self.base))
# If we're still here, the base branch exists.
self._base_exists = True
except GitError:
# If we failed to verify, the base branch doesn't exist.
self._base_exists = False
return self._base_exists
def UploadForReview(self, people,
auto_topic=False,
draft=False,
@ -226,6 +250,7 @@ class DiffColoring(Coloring):
def __init__(self, config):
Coloring.__init__(self, config, 'diff')
self.project = self.printer('header', attr='bold')
self.fail = self.printer('fail', fg='red')
class _Annotation(object):
@ -236,17 +261,70 @@ class _Annotation(object):
self.keep = keep
class _CopyFile(object):
def _SafeExpandPath(base, subpath, skipfinal=False):
"""Make sure |subpath| is completely safe under |base|.
def __init__(self, src, dest, abssrc, absdest):
We make sure no intermediate symlinks are traversed, and that the final path
is not a special file (e.g. not a socket or fifo).
NB: We rely on a number of paths already being filtered out while parsing the
manifest. See the validation logic in manifest_xml.py for more details.
"""
components = subpath.split(os.path.sep)
if skipfinal:
# Whether the caller handles the final component itself.
finalpart = components.pop()
path = base
for part in components:
if part in {'.', '..'}:
raise ManifestInvalidPathError(
'%s: "%s" not allowed in paths' % (subpath, part))
path = os.path.join(path, part)
if platform_utils.islink(path):
raise ManifestInvalidPathError(
'%s: traversing symlinks not allow' % (path,))
if os.path.exists(path):
if not os.path.isfile(path) and not platform_utils.isdir(path):
raise ManifestInvalidPathError(
'%s: only regular files & directories allowed' % (path,))
if skipfinal:
path = os.path.join(path, finalpart)
return path
class _CopyFile(object):
"""Container for <copyfile> manifest element."""
def __init__(self, git_worktree, src, topdir, dest):
"""Register a <copyfile> request.
Args:
git_worktree: Absolute path to the git project checkout.
src: Relative path under |git_worktree| of file to read.
topdir: Absolute path to the top of the repo client checkout.
dest: Relative path under |topdir| of file to write.
"""
self.git_worktree = git_worktree
self.topdir = topdir
self.src = src
self.dest = dest
self.abs_src = abssrc
self.abs_dest = absdest
def _Copy(self):
src = self.abs_src
dest = self.abs_dest
src = _SafeExpandPath(self.git_worktree, self.src)
dest = _SafeExpandPath(self.topdir, self.dest)
if platform_utils.isdir(src):
raise ManifestInvalidPathError(
'%s: copying from directory not supported' % (self.src,))
if platform_utils.isdir(dest):
raise ManifestInvalidPathError(
'%s: copying to directory not allowed' % (self.dest,))
# copy file if it does not exist or is out of date
if not os.path.exists(dest) or not filecmp.cmp(src, dest):
try:
@ -267,13 +345,21 @@ class _CopyFile(object):
class _LinkFile(object):
"""Container for <linkfile> manifest element."""
def __init__(self, git_worktree, src, dest, relsrc, absdest):
def __init__(self, git_worktree, src, topdir, dest):
"""Register a <linkfile> request.
Args:
git_worktree: Absolute path to the git project checkout.
src: Target of symlink relative to path under |git_worktree|.
topdir: Absolute path to the top of the repo client checkout.
dest: Relative path under |topdir| of symlink to create.
"""
self.git_worktree = git_worktree
self.topdir = topdir
self.src = src
self.dest = dest
self.src_rel_to_dest = relsrc
self.abs_dest = absdest
def __linkIt(self, relSrc, absDest):
# link file if it does not exist or is out of date
@ -291,35 +377,42 @@ class _LinkFile(object):
_error('Cannot link file %s to %s', relSrc, absDest)
def _Link(self):
"""Link the self.rel_src_to_dest and self.abs_dest. Handles wild cards
on the src linking all of the files in the source in to the destination
directory.
"""Link the self.src & self.dest paths.
Handles wild cards on the src linking all of the files in the source in to
the destination directory.
"""
# We use the absSrc to handle the situation where the current directory
# is not the root of the repo
absSrc = os.path.join(self.git_worktree, self.src)
if os.path.exists(absSrc):
# Entity exists so just a simple one to one link operation
self.__linkIt(self.src_rel_to_dest, self.abs_dest)
# 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):
# Entity exists so just a simple one to one link operation.
dest = _SafeExpandPath(self.topdir, self.dest, skipfinal=True)
# dest & src are absolute paths at this point. Make sure the target of
# the symlink is relative in the context of the repo client checkout.
relpath = os.path.relpath(src, os.path.dirname(dest))
self.__linkIt(relpath, dest)
else:
dest = _SafeExpandPath(self.topdir, self.dest)
# Entity doesn't exist assume there is a wild card
absDestDir = self.abs_dest
if os.path.exists(absDestDir) and not platform_utils.isdir(absDestDir):
_error('Link error: src with wildcard, %s must be a directory',
absDestDir)
if os.path.exists(dest) and not platform_utils.isdir(dest):
_error('Link error: src with wildcard, %s must be a directory', dest)
else:
absSrcFiles = glob.glob(absSrc)
for absSrcFile in absSrcFiles:
for absSrcFile in glob.glob(src):
# Create a releative path from source dir to destination dir
absSrcDir = os.path.dirname(absSrcFile)
relSrcDir = os.path.relpath(absSrcDir, absDestDir)
relSrcDir = os.path.relpath(absSrcDir, dest)
# Get the source file name
srcFile = os.path.basename(absSrcFile)
# Now form the final full paths to srcFile. They will be
# absolute for the desintaiton and relative for the srouce.
absDest = os.path.join(absDestDir, srcFile)
absDest = os.path.join(dest, srcFile)
relSrc = os.path.join(relSrcDir, srcFile)
self.__linkIt(relSrc, absDest)
@ -542,6 +635,105 @@ class RepoHook(object):
prompt % (self._GetMustVerb(), self._script_fullpath),
'Scripts have changed since %s was allowed.' % (self._hook_type,))
@staticmethod
def _ExtractInterpFromShebang(data):
"""Extract the interpreter used in the shebang.
Try to locate the interpreter the script is using (ignoring `env`).
Args:
data: The file content of the script.
Returns:
The basename of the main script interpreter, or None if a shebang is not
used or could not be parsed out.
"""
firstline = data.splitlines()[:1]
if not firstline:
return None
# The format here can be tricky.
shebang = firstline[0].strip()
m = re.match(r'^#!\s*([^\s]+)(?:\s+([^\s]+))?', shebang)
if not m:
return None
# If the using `env`, find the target program.
interp = m.group(1)
if os.path.basename(interp) == 'env':
interp = m.group(2)
return interp
def _ExecuteHookViaReexec(self, interp, context, **kwargs):
"""Execute the hook script through |interp|.
Note: Support for this feature should be dropped ~Jun 2021.
Args:
interp: The Python program to run.
context: Basic Python context to execute the hook inside.
kwargs: Arbitrary arguments to pass to the hook script.
Raises:
HookError: When the hooks failed for any reason.
"""
# This logic needs to be kept in sync with _ExecuteHookViaImport below.
script = """
import json, os, sys
path = '''%(path)s'''
kwargs = json.loads('''%(kwargs)s''')
context = json.loads('''%(context)s''')
sys.path.insert(0, os.path.dirname(path))
data = open(path).read()
exec(compile(data, path, 'exec'), context)
context['main'](**kwargs)
""" % {
'path': self._script_fullpath,
'kwargs': json.dumps(kwargs),
'context': json.dumps(context),
}
# We pass the script via stdin to avoid OS argv limits. It also makes
# unhandled exception tracebacks less verbose/confusing for users.
cmd = [interp, '-c', 'import sys; exec(sys.stdin.read())']
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
proc.communicate(input=script.encode('utf-8'))
if proc.returncode:
raise HookError('Failed to run %s hook.' % (self._hook_type,))
def _ExecuteHookViaImport(self, data, context, **kwargs):
"""Execute the hook code in |data| directly.
Args:
data: The code of the hook to execute.
context: Basic Python context to execute the hook inside.
kwargs: Arbitrary arguments to pass to the hook script.
Raises:
HookError: When the hooks failed for any reason.
"""
# Exec, storing global context in the context dict. We catch exceptions
# and convert to a HookError w/ just the failing traceback.
try:
exec(compile(data, self._script_fullpath, 'exec'), context)
except Exception:
raise HookError('%s\nFailed to import %s hook; see traceback above.' %
(traceback.format_exc(), self._hook_type))
# Running the script should have defined a main() function.
if 'main' not in context:
raise HookError('Missing main() in: "%s"' % self._script_fullpath)
# Call the main function in the hook. If the hook should cause the
# build to fail, it will raise an Exception. We'll catch that convert
# to a HookError w/ just the failing traceback.
try:
context['main'](**kwargs)
except Exception:
raise HookError('%s\nFailed to run main() for %s hook; see traceback '
'above.' % (traceback.format_exc(), self._hook_type))
def _ExecuteHook(self, **kwargs):
"""Actually execute the given hook.
@ -566,19 +758,8 @@ class RepoHook(object):
# hooks can't import repo files.
sys.path = [os.path.dirname(self._script_fullpath)] + sys.path[1:]
# Exec, storing global context in the context dict. We catch exceptions
# and convert to a HookError w/ just the failing traceback.
# Initial global context for the hook to run within.
context = {'__file__': self._script_fullpath}
try:
exec(compile(open(self._script_fullpath).read(),
self._script_fullpath, 'exec'), context)
except Exception:
raise HookError('%s\nFailed to import %s hook; see traceback above.' %
(traceback.format_exc(), self._hook_type))
# Running the script should have defined a main() function.
if 'main' not in context:
raise HookError('Missing main() in: "%s"' % self._script_fullpath)
# Add 'hook_should_take_kwargs' to the arguments to be passed to main.
# We don't actually want hooks to define their main with this argument--
@ -590,15 +771,31 @@ class RepoHook(object):
kwargs = kwargs.copy()
kwargs['hook_should_take_kwargs'] = True
# Call the main function in the hook. If the hook should cause the
# build to fail, it will raise an Exception. We'll catch that convert
# to a HookError w/ just the failing traceback.
try:
context['main'](**kwargs)
except Exception:
raise HookError('%s\nFailed to run main() for %s hook; see traceback '
'above.' % (traceback.format_exc(),
self._hook_type))
# See what version of python the hook has been written against.
data = open(self._script_fullpath).read()
interp = self._ExtractInterpFromShebang(data)
reexec = False
if interp:
prog = os.path.basename(interp)
if prog.startswith('python2') and sys.version_info.major != 2:
reexec = True
elif prog.startswith('python3') and sys.version_info.major == 2:
reexec = True
# Attempt to execute the hooks through the requested version of Python.
if reexec:
try:
self._ExecuteHookViaReexec(interp, context, **kwargs)
except OSError as e:
if e.errno == errno.ENOENT:
# We couldn't find the interpreter, so fallback to importing.
reexec = False
else:
raise
# Run the hook by importing directly.
if not reexec:
self._ExecuteHookViaImport(data, context, **kwargs)
finally:
# Restore sys.path and CWD.
sys.path = orig_syspath
@ -757,10 +954,17 @@ class Project(object):
@property
def CurrentBranch(self):
"""Obtain the name of the currently checked out branch.
The branch name omits the 'refs/heads/' prefix.
None is returned if the project is on a detached HEAD.
The branch name omits the 'refs/heads/' prefix.
None is returned if the project is on a detached HEAD, or if the work_git is
otheriwse inaccessible (e.g. an incomplete sync).
"""
b = self.work_git.GetHead()
try:
b = self.work_git.GetHead()
except NoManifestException:
# If the local checkout is in a bad state, don't barf. Let the callers
# process this like the head is unreadable.
return None
if b.startswith(R_HEADS):
return b[len(R_HEADS):]
return None
@ -929,7 +1133,7 @@ class Project(object):
"""Prints the status of the repository to stdout.
Args:
output: If specified, redirect the output to this object.
output_redir: If specified, redirect the output to this object.
quiet: If True then only print the project name. Do not print
the modified files, branch name, etc.
"""
@ -1028,19 +1232,29 @@ class Project(object):
cmd.append('--src-prefix=a/%s/' % self.relpath)
cmd.append('--dst-prefix=b/%s/' % self.relpath)
cmd.append('--')
p = GitCommand(self,
cmd,
capture_stdout=True,
capture_stderr=True)
try:
p = GitCommand(self,
cmd,
capture_stdout=True,
capture_stderr=True)
except GitError as e:
out.nl()
out.project('project %s/' % self.relpath)
out.nl()
out.fail('%s', str(e))
out.nl()
return False
has_diff = False
for line in p.process.stdout:
if not hasattr(line, 'encode'):
line = line.decode()
if not has_diff:
out.nl()
out.project('project %s/' % self.relpath)
out.nl()
has_diff = True
print(line[:-1])
p.Wait()
return p.Wait() == 0
# Publish / Upload ##
@ -1172,10 +1386,11 @@ class Project(object):
ref_spec = '%s:refs/%s/%s' % (R_HEADS + branch.name, upload_type,
dest_branch)
opts = []
if auto_topic:
ref_spec = ref_spec + '/' + branch.name
opts += ['topic=' + branch.name]
opts = ['r=%s' % p for p in people[0]]
opts += ['r=%s' % p for p in people[0]]
opts += ['cc=%s' % p for p in people[1]]
if notify:
opts += ['notify=' + notify]
@ -1223,7 +1438,8 @@ class Project(object):
archive=False,
optimized_fetch=False,
prune=False,
submodules=False):
submodules=False,
clone_filter=None):
"""Perform only the network IO portion of the sync process.
Local working directory/branch state is not affected.
"""
@ -1265,12 +1481,9 @@ class Project(object):
if is_new:
alt = os.path.join(self.gitdir, 'objects/info/alternates')
try:
fd = open(alt)
try:
with open(alt) as fd:
# This works for both absolute and relative alternate directories.
alt_dir = os.path.join(self.objdir, 'objects', fd.readline().rstrip())
finally:
fd.close()
except IOError:
alt_dir = None
else:
@ -1306,7 +1519,8 @@ class Project(object):
not self._RemoteFetch(initial=is_new, quiet=quiet, alt_dir=alt_dir,
current_branch_only=current_branch_only,
no_tags=no_tags, prune=prune, depth=depth,
submodules=submodules)):
submodules=submodules, force_sync=force_sync,
clone_filter=clone_filter)):
return False
mp = self.manifest.manifestProject
@ -1376,6 +1590,13 @@ class Project(object):
"""Perform only the local IO portion of the sync process.
Network access is not required.
"""
if not os.path.exists(self.gitdir):
syncbuf.fail(self,
'Cannot checkout %s due to missing network sync; Run '
'`repo sync -n %s` first.' %
(self.name, self.name))
return
self._InitWorkTree(force_sync=force_sync, submodules=submodules)
all_refs = self.bare_ref.all
self.CleanPublishedCache(all_refs)
@ -1456,7 +1677,16 @@ class Project(object):
return
upstream_gain = self._revlist(not_rev(HEAD), revid)
pub = self.WasPublished(branch.name, all_refs)
# See if we can perform a fast forward merge. This can happen if our
# branch isn't in the exact same state as we last published.
try:
self.work_git.merge_base('--is-ancestor', HEAD, revid)
# Skip the published logic.
pub = False
except GitError:
pub = self.WasPublished(branch.name, all_refs)
if pub:
not_merged = self._revlist(not_rev(revid), pub)
if not_merged:
@ -1485,7 +1715,7 @@ class Project(object):
last_mine = None
cnt_mine = 0
for commit in local_changes:
commit_id, committer_email = commit.decode('utf-8').split(' ', 1)
commit_id, committer_email = commit.split(' ', 1)
if committer_email == self.UserEmail:
last_mine = commit_id
cnt_mine += 1
@ -1550,18 +1780,25 @@ class Project(object):
if submodules:
syncbuf.later1(self, _dosubmodules)
def AddCopyFile(self, src, dest, absdest):
# dest should already be an absolute path, but src is project relative
# make src an absolute path
abssrc = os.path.join(self.worktree, src)
self.copyfiles.append(_CopyFile(src, dest, abssrc, absdest))
def AddCopyFile(self, src, dest, topdir):
"""Mark |src| for copying to |dest| (relative to |topdir|).
def AddLinkFile(self, src, dest, absdest):
# dest should already be an absolute path, but src is project relative
# make src relative path to dest
absdestdir = os.path.dirname(absdest)
relsrc = os.path.relpath(os.path.join(self.worktree, src), absdestdir)
self.linkfiles.append(_LinkFile(self.worktree, src, dest, relsrc, absdest))
No filesystem changes occur here. Actual copying happens later on.
Paths should have basic validation run on them before being queued.
Further checking will be handled when the actual copy happens.
"""
self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest))
def AddLinkFile(self, src, dest, topdir):
"""Mark |dest| to create a symlink (relative to |topdir|) pointing to |src|.
No filesystem changes occur here. Actual linking happens later on.
Paths should have basic validation run on them before being queued.
Further checking will be handled when the actual link happens.
"""
self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
def AddAnnotation(self, name, value, keep):
self.annotations.append(_Annotation(name, value, keep))
@ -1585,7 +1822,19 @@ class Project(object):
# Branch Management ##
def StartBranch(self, name, branch_merge=''):
def GetHeadPath(self):
"""Return the full path to the HEAD ref."""
dotgit = os.path.join(self.worktree, '.git')
if os.path.isfile(dotgit):
# Git worktrees use a "gitdir:" syntax to point to the scratch space.
with open(dotgit) as fp:
setting = fp.read()
assert setting.startswith('gitdir:')
gitdir = setting.split(':', 1)[1].strip()
dotgit = os.path.join(self.worktree, gitdir)
return os.path.join(dotgit, HEAD)
def StartBranch(self, name, branch_merge='', revision=None):
"""Create a new branch off the manifest's revision.
"""
if not branch_merge:
@ -1606,7 +1855,11 @@ class Project(object):
branch.merge = branch_merge
if not branch.merge.startswith('refs/') and not ID_RE.match(branch_merge):
branch.merge = R_HEADS + branch_merge
revid = self.GetRevisionId(all_refs)
if revision is None:
revid = self.GetRevisionId(all_refs)
else:
revid = self.work_git.rev_parse(revision)
if head.startswith(R_HEADS):
try:
@ -1620,8 +1873,7 @@ class Project(object):
except OSError:
pass
_lwrite(ref, '%s\n' % revid)
_lwrite(os.path.join(self.worktree, '.git', HEAD),
'ref: %s%s\n' % (R_HEADS, name))
_lwrite(self.GetHeadPath(), 'ref: %s%s\n' % (R_HEADS, name))
branch.Save()
return True
@ -1668,8 +1920,7 @@ class Project(object):
# Same revision; just update HEAD to point to the new
# target branch, but otherwise take no other action.
#
_lwrite(os.path.join(self.worktree, '.git', HEAD),
'ref: %s%s\n' % (R_HEADS, name))
_lwrite(self.GetHeadPath(), 'ref: %s%s\n' % (R_HEADS, name))
return True
return GitCommand(self,
@ -1702,8 +1953,7 @@ class Project(object):
revid = self.GetRevisionId(all_refs)
if head == revid:
_lwrite(os.path.join(self.worktree, '.git', HEAD),
'%s\n' % revid)
_lwrite(self.GetHeadPath(), '%s\n' % revid)
else:
self._Checkout(revid, quiet=True)
@ -1807,8 +2057,8 @@ class Project(object):
submodules.append((sub_rev, sub_path, sub_url))
return submodules
re_path = re.compile(r'^submodule\.([^.]+)\.path=(.*)$')
re_url = re.compile(r'^submodule\.([^.]+)\.url=(.*)$')
re_path = re.compile(r'^submodule\.(.+)\.path=(.*)$')
re_url = re.compile(r'^submodule\.(.+)\.url=(.*)$')
def parse_gitmodules(gitdir, rev):
cmd = ['cat-file', 'blob', '%s:.gitmodules' % rev]
@ -1823,7 +2073,7 @@ class Project(object):
gitmodules_lines = []
fd, temp_gitmodules_path = tempfile.mkstemp()
try:
os.write(fd, p.stdout)
os.write(fd, p.stdout.encode('utf-8'))
os.close(fd)
cmd = ['config', '--file', temp_gitmodules_path, '--list']
p = GitCommand(None, cmd, capture_stdout=True, capture_stderr=True,
@ -1955,7 +2205,9 @@ class Project(object):
no_tags=False,
prune=False,
depth=None,
submodules=False):
submodules=False,
force_sync=False,
clone_filter=None):
is_sha1 = False
tag_name = None
@ -1979,8 +2231,9 @@ class Project(object):
if is_sha1 or tag_name is not None:
if self._CheckForImmutableRevision():
print('Skipped fetching project %s (already have persistent ref)'
% self.name)
if not quiet:
print('Skipped fetching project %s (already have persistent ref)'
% self.name)
return True
if is_sha1 and not depth:
# When syncing a specific commit and --depth is not set:
@ -2045,6 +2298,11 @@ class Project(object):
cmd = ['fetch']
if clone_filter:
git_require((2, 19, 0), fail=True, msg='partial clones')
cmd.append('--filter=%s' % clone_filter)
self.config.SetString('extensions.partialclone', self.remote.name)
if depth:
cmd.append('--depth=%s' % depth)
else:
@ -2061,12 +2319,8 @@ class Project(object):
cmd.append('--update-head-ok')
cmd.append(name)
# If using depth then we should not get all the tags since they may
# be outside of the depth.
if no_tags or depth:
cmd.append('--no-tags')
else:
cmd.append('--tags')
if force_sync:
cmd.append('--force')
if prune:
cmd.append('--prune')
@ -2082,19 +2336,36 @@ class Project(object):
spec.append('tag')
spec.append(tag_name)
if not self.manifest.IsMirror:
if self.manifest.IsMirror and not current_branch_only:
branch = None
else:
branch = self.revisionExpr
if is_sha1 and depth and git_require((1, 8, 3)):
# Shallow checkout of a specific commit, fetch from that commit and not
# the heads only as the commit might be deeper in the history.
spec.append(branch)
else:
if is_sha1:
branch = self.upstream
if branch is not None and branch.strip():
if not branch.startswith('refs/'):
branch = R_HEADS + branch
spec.append(str((u'+%s:' % branch) + remote.ToLocal(branch)))
if (not self.manifest.IsMirror and is_sha1 and depth
and git_require((1, 8, 3))):
# Shallow checkout of a specific commit, fetch from that commit and not
# the heads only as the commit might be deeper in the history.
spec.append(branch)
else:
if is_sha1:
branch = self.upstream
if branch is not None and branch.strip():
if not branch.startswith('refs/'):
branch = R_HEADS + branch
spec.append(str((u'+%s:' % branch) + remote.ToLocal(branch)))
# If mirroring repo and we cannot deduce the tag or branch to fetch, fetch
# whole repo.
if self.manifest.IsMirror and not spec:
spec.append(str((u'+refs/heads/*:') + remote.ToLocal('refs/heads/*')))
# If using depth then we should not get all the tags since they may
# be outside of the depth.
if no_tags or depth:
cmd.append('--no-tags')
else:
cmd.append('--tags')
spec.append(str((u'+refs/tags/*:') + remote.ToLocal('refs/tags/*')))
cmd.extend(spec)
ok = False
@ -2142,12 +2413,12 @@ class Project(object):
return self._RemoteFetch(name=name,
current_branch_only=current_branch_only,
initial=False, quiet=quiet, alt_dir=alt_dir,
depth=None)
depth=None, clone_filter=clone_filter)
else:
# Avoid infinite recursion: sync all branches with depth set to None
return self._RemoteFetch(name=name, current_branch_only=False,
initial=False, quiet=quiet, alt_dir=alt_dir,
depth=None)
depth=None, clone_filter=clone_filter)
return ok
@ -2209,13 +2480,17 @@ class Project(object):
cmd += ['--continue-at', '%d' % (size,)]
else:
platform_utils.remove(tmpPath)
if 'http_proxy' in os.environ and 'darwin' == sys.platform:
cmd += ['--proxy', os.environ['http_proxy']]
with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, _proxy):
with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
if cookiefile:
cmd += ['--cookie', cookiefile, '--cookie-jar', cookiefile]
if srcUrl.startswith('persistent-'):
srcUrl = srcUrl[len('persistent-'):]
cmd += ['--cookie', cookiefile]
if proxy:
cmd += ['--proxy', proxy]
elif 'http_proxy' in os.environ and 'darwin' == sys.platform:
cmd += ['--proxy', os.environ['http_proxy']]
if srcUrl.startswith('persistent-https'):
srcUrl = 'http' + srcUrl[len('persistent-https'):]
elif srcUrl.startswith('persistent-http'):
srcUrl = 'http' + srcUrl[len('persistent-http'):]
cmd += [srcUrl]
if IsTrace():
@ -2249,8 +2524,8 @@ class Project(object):
def _IsValidBundle(self, path, quiet):
try:
with open(path) as f:
if f.read(16) == '# v2 git bundle\n':
with open(path, 'rb') as f:
if f.read(16) == b'# v2 git bundle\n':
return True
else:
if not quiet:
@ -2281,10 +2556,7 @@ class Project(object):
cmd = ['ls-remote', self.remote.name, refs]
p = GitCommand(self, cmd, capture_stdout=True)
if p.Wait() == 0:
if hasattr(p.stdout, 'decode'):
return p.stdout.decode('utf-8')
else:
return p.stdout
return p.stdout
return None
def _Revert(self, rev):
@ -2393,6 +2665,7 @@ class Project(object):
if m.Has(key, include_defaults=False):
self.config.SetString(key, m.GetString(key))
self.config.SetString('filter.lfs.smudge', 'git-lfs smudge --skip -- %f')
self.config.SetString('filter.lfs.process', 'git-lfs filter-process --skip')
if self.manifest.IsMirror:
self.config.SetString('core.bare', 'true')
else:
@ -2558,41 +2831,45 @@ class Project(object):
raise
def _InitWorkTree(self, force_sync=False, submodules=False):
dotgit = os.path.join(self.worktree, '.git')
init_dotgit = not os.path.exists(dotgit)
realdotgit = os.path.join(self.worktree, '.git')
tmpdotgit = realdotgit + '.tmp'
init_dotgit = not os.path.exists(realdotgit)
if init_dotgit:
dotgit = tmpdotgit
platform_utils.rmtree(tmpdotgit, ignore_errors=True)
os.makedirs(tmpdotgit)
self._ReferenceGitDir(self.gitdir, tmpdotgit, share_refs=True,
copy_all=False)
else:
dotgit = realdotgit
try:
if init_dotgit:
os.makedirs(dotgit)
self._ReferenceGitDir(self.gitdir, dotgit, share_refs=True,
copy_all=False)
self._CheckDirReference(self.gitdir, dotgit, share_refs=True)
except GitError as e:
if force_sync and not init_dotgit:
try:
platform_utils.rmtree(dotgit)
return self._InitWorkTree(force_sync=False, submodules=submodules)
except:
raise e
raise e
try:
self._CheckDirReference(self.gitdir, dotgit, share_refs=True)
except GitError as e:
if force_sync:
try:
platform_utils.rmtree(dotgit)
return self._InitWorkTree(force_sync=False, submodules=submodules)
except:
raise e
raise e
if init_dotgit:
_lwrite(os.path.join(tmpdotgit, HEAD), '%s\n' % self.GetRevisionId())
if init_dotgit:
_lwrite(os.path.join(dotgit, HEAD), '%s\n' % self.GetRevisionId())
# Now that the .git dir is fully set up, move it to its final home.
platform_utils.rename(tmpdotgit, realdotgit)
cmd = ['read-tree', '--reset', '-u']
cmd.append('-v')
cmd.append(HEAD)
if GitCommand(self, cmd).Wait() != 0:
raise GitError("cannot initialize work tree")
# Finish checking out the worktree.
cmd = ['read-tree', '--reset', '-u']
cmd.append('-v')
cmd.append(HEAD)
if GitCommand(self, cmd).Wait() != 0:
raise GitError('Cannot initialize work tree for ' + self.name)
if submodules:
self._SyncSubmodules(quiet=True)
self._CopyAndLinkFiles()
except Exception:
if init_dotgit:
platform_utils.rmtree(dotgit)
raise
if submodules:
self._SyncSubmodules(quiet=True)
self._CopyAndLinkFiles()
def _get_symlink_error_message(self):
if platform_utils.isWindows():
@ -2684,6 +2961,7 @@ class Project(object):
def DiffZ(self, name, *args):
cmd = [name]
cmd.append('-z')
cmd.append('--ignore-submodules')
cmd.extend(args)
p = GitCommand(self._project,
cmd,
@ -2693,6 +2971,8 @@ class Project(object):
capture_stderr=True)
try:
out = p.process.stdout.read()
if not hasattr(out, 'encode'):
out = out.decode()
r = {}
if out:
out = iter(out[:-1].split('\0'))
@ -2736,15 +3016,12 @@ class Project(object):
if self._bare:
path = os.path.join(self._project.gitdir, HEAD)
else:
path = os.path.join(self._project.worktree, '.git', HEAD)
path = self._project.GetHeadPath()
try:
fd = open(path)
with open(path) as fd:
line = fd.readline()
except IOError as e:
raise NoManifestException(path, str(e))
try:
line = fd.readline()
finally:
fd.close()
try:
line = line.decode()
except AttributeError:
@ -2801,15 +3078,10 @@ class Project(object):
gitdir=self._gitdir,
capture_stdout=True,
capture_stderr=True)
r = []
for line in p.process.stdout:
if line[-1] == '\n':
line = line[:-1]
r.append(line)
if p.Wait() != 0:
raise GitError('%s rev-list %s: %s' %
(self._project.name, str(args), p.stderr))
return r
return p.stdout.splitlines()
def __getattr__(self, name):
"""Allow arbitrary git commands using pythonic syntax.
@ -2857,10 +3129,6 @@ class Project(object):
raise GitError('%s %s: %s' %
(self._project.name, name, p.stderr))
r = p.stdout
try:
r = r.decode('utf-8')
except AttributeError:
pass
if r.endswith('\n') and r.index('\n') == len(r) - 1:
return r[:-1]
return r
@ -2988,6 +3256,11 @@ class SyncBuffer(object):
return True
def _PrintMessages(self):
if self._messages or self._failures:
if os.isatty(2):
self.out.write(progress.CSI_ERASE_LINE)
self.out.write('\r')
for m in self._messages:
m.Print(self)
for m in self._failures:

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2013 The Android Open Source Project
#

383
repo
View File

@ -1,4 +1,92 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
"""Repo launcher.
This is a standalone tool that people may copy to anywhere in their system.
It is used to get an initial repo client checkout, and after that it runs the
copy of repo in the checkout.
"""
from __future__ import print_function
import os
import platform
import subprocess
import sys
def exec_command(cmd):
"""Execute |cmd| or return None on failure."""
try:
if platform.system() == 'Windows':
ret = subprocess.call(cmd)
sys.exit(ret)
else:
os.execvp(cmd[0], cmd)
except:
pass
def check_python_version():
"""Make sure the active Python version is recent enough."""
def reexec(prog):
exec_command([prog] + sys.argv)
MIN_PYTHON_VERSION = (3, 6)
ver = sys.version_info
major = ver.major
minor = ver.minor
# Abort on very old Python 2 versions.
if (major, minor) < (2, 7):
print('repo: error: Your Python version is too old. '
'Please use Python {}.{} or newer instead.'.format(
*MIN_PYTHON_VERSION), file=sys.stderr)
sys.exit(1)
# Try to re-exec the version specific Python 3 if needed.
if (major, minor) < MIN_PYTHON_VERSION:
# Python makes releases ~once a year, so try our min version +10 to help
# bridge the gap. This is the fallback anyways so perf isn't critical.
min_major, min_minor = MIN_PYTHON_VERSION
for inc in range(0, 10):
reexec('python{}.{}'.format(min_major, min_minor + inc))
# Try the generic Python 3 wrapper, but only if it's new enough. We don't
# want to go from (still supported) Python 2.7 to (unsupported) Python 3.5.
try:
proc = subprocess.Popen(
['python3', '-c', 'import sys; '
'print(sys.version_info.major, sys.version_info.minor)'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, _) = proc.communicate()
python3_ver = tuple(int(x) for x in output.decode('utf-8').split())
except (OSError, subprocess.CalledProcessError):
python3_ver = None
# The python3 version looks like it's new enough, so give it a try.
if python3_ver and python3_ver >= MIN_PYTHON_VERSION:
reexec('python3')
# We're still here, so diagnose things for the user.
if major < 3:
print('repo: warning: Python 2 is no longer supported; '
'Please upgrade to Python {}.{}+.'.format(*MIN_PYTHON_VERSION),
file=sys.stderr)
else:
print('repo: error: Python 3 version is too old; '
'Please use Python {}.{} or newer.'.format(*MIN_PYTHON_VERSION),
file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
# TODO(vapier): Enable this on Windows once we have Python 3 issues fixed.
if platform.system() != 'Windows':
check_python_version()
# repo default configuration
#
@ -6,7 +94,9 @@ import os
REPO_URL = os.environ.get('REPO_URL', None)
if not REPO_URL:
REPO_URL = 'https://gerrit.googlesource.com/git-repo'
REPO_REV = 'stable'
REPO_REV = os.environ.get('REPO_REV')
if not REPO_REV:
REPO_REV = 'stable'
# Copyright (C) 2008 Google Inc.
#
@ -23,10 +113,10 @@ REPO_REV = 'stable'
# limitations under the License.
# increment this whenever we make important changes to this script
VERSION = (1, 24)
VERSION = (2, 0)
# increment this if the MAINTAINER_KEYS block is modified
KEYRING_VERSION = (1, 2)
KEYRING_VERSION = (2, 0)
# Each individual key entry is created by using:
# gpg --armor --export keyid
@ -72,60 +162,30 @@ HTHs37+/QLMomGEGKZMWi0dShU2J5mNRQu3Hhxl3hHDVbt5CeJBb26aQcQrFz69W
zE3GNvmJosh6leayjtI9P2A6iEkEGBECAAkFAkj3uiACGwwACgkQFlMNXpIPXGWp
TACbBS+Up3RpfYVfd63c1cDdlru13pQAn3NQy/SN858MkxN+zym86UBgOad2
=CMiZ
-----END PGP PUBLIC KEY BLOCK-----
Conley Owens <cco3@android.com>
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1.4.11 (GNU/Linux)
mQENBFHRvc8BCADFg45Xx/y6QDC+T7Y/gGc7vx0ww7qfOwIKlAZ9xG3qKunMxo+S
hPCnzEl3cq+6I1Ww/ndop/HB3N3toPXRCoN8Vs4/Hc7by+SnaLFnacrm+tV5/OgT
V37Lzt8lhay1Kl+YfpFwHYYpIEBLFV9knyfRXS/428W2qhdzYfvB15/AasRmwmor
py4NIzSs8UD/SPr1ihqNCdZM76+MQyN5HMYXW/ALZXUFG0pwluHFA7hrfPG74i8C
zMiP7qvMWIl/r/jtzHioH1dRKgbod+LZsrDJ8mBaqsZaDmNJMhss9g76XvfMyLra
9DI9/iFuBpGzeqBv0hwOGQspLRrEoyTeR6n1ABEBAAG0H0NvbmxleSBPd2VucyA8
Y2NvM0BhbmRyb2lkLmNvbT6JATgEEwECACIFAlHRvc8CGwMGCwkIBwMCBhUIAgkK
CwQWAgMBAh4BAheAAAoJEGe35EhpKzgsP6AIAJKJmNtn4l7hkYHKHFSo3egb6RjQ
zEIP3MFTcu8HFX1kF1ZFbrp7xqurLaE53kEkKuAAvjJDAgI8mcZHP1JyplubqjQA
xvv84gK+OGP3Xk+QK1ZjUQSbjOpjEiSZpRhWcHci3dgOUH4blJfByHw25hlgHowd
a/2PrNKZVcJ92YienaxxGjcXEUcd0uYEG2+rwllQigFcnMFDhr9B71MfalRHjFKE
fmdoypqLrri61YBc59P88Rw2/WUpTQjgNubSqa3A2+CKdaRyaRw+2fdF4TdR0h8W
zbg+lbaPtJHsV+3mJC7fq26MiJDRJa5ZztpMn8su20gbLgi2ShBOaHAYDDi5AQ0E
UdG9zwEIAMoOBq+QLNozAhxOOl5GL3StTStGRgPRXINfmViTsihrqGCWBBUfXlUE
OytC0mYcrDUQev/8ToVoyqw+iGSwDkcSXkrEUCKFtHV/GECWtk1keyHgR10YKI1R
mquSXoubWGqPeG1PAI74XWaRx8UrL8uCXUtmD8Q5J7mDjKR5NpxaXrwlA0bKsf2E
Gp9tu1kKauuToZhWHMRMqYSOGikQJwWSFYKT1KdNcOXLQF6+bfoJ6sjVYdwfmNQL
Ixn8QVhoTDedcqClSWB17VDEFDFa7MmqXZz2qtM3X1R/MUMHqPtegQzBGNhRdnI2
V45+1Nnx/uuCxDbeI4RbHzujnxDiq70AEQEAAYkBHwQYAQIACQUCUdG9zwIbDAAK
CRBnt+RIaSs4LNVeB/0Y2pZ8I7gAAcEM0Xw8drr4omg2fUoK1J33ozlA/RxeA/lJ
I3KnyCDTpXuIeBKPGkdL8uMATC9Z8DnBBajRlftNDVZS3Hz4G09G9QpMojvJkFJV
By+01Flw/X+eeN8NpqSuLV4W+AjEO8at/VvgKr1AFvBRdZ7GkpI1o6DgPe7ZqX+1
dzQZt3e13W0rVBb/bUgx9iSLoeWP3aq/k+/GRGOR+S6F6BBSl0SQ2EF2+dIywb1x
JuinEP+AwLAUZ1Bsx9ISC0Agpk2VeHXPL3FGhroEmoMvBzO0kTFGyoeT7PR/BfKv
+H/g3HsL2LOB9uoIm8/5p2TTU5ttYCXMHhQZ81AY
=AUp4
-----END PGP PUBLIC KEY BLOCK-----
"""
GIT = 'git' # our git command
# 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
S_repo = 'repo' # special repo repository
S_manifests = 'manifests' # special manifest repository
REPO_MAIN = S_repo + '/main.py' # main script
MIN_PYTHON_VERSION = (2, 6) # minimum supported python version
GITC_CONFIG_FILE = '/gitc/.config'
GITC_FS_ROOT_DIR = '/gitc/manifest-rw/'
import collections
import errno
import optparse
import platform
import re
import shutil
import stat
import subprocess
import sys
if sys.version_info[0] == 3:
import urllib.request
@ -138,25 +198,6 @@ else:
urllib.error = urllib2
def _print(*objects, **kwargs):
sep = kwargs.get('sep', ' ')
end = kwargs.get('end', '\n')
out = kwargs.get('file', sys.stdout)
out.write(sep.join(objects) + end)
# On Windows stderr is buffered, so flush to maintain the order of error messages.
if out == sys.stderr and platform.system() == "Windows":
out.flush()
# Python version check
ver = sys.version_info
if (ver[0], ver[1]) < MIN_PYTHON_VERSION:
_print('error: Python version %s unsupported.\n'
'Please use Python 2.6 - 2.7 instead.'
% sys.version.split(' ')[0], file=sys.stderr)
sys.exit(1)
home_dot_repo = os.path.expanduser('~/.repoconfig')
gpg_dir = os.path.join(home_dot_repo, 'gnupg')
@ -180,7 +221,7 @@ group.add_option('-b', '--manifest-branch',
group.add_option('-m', '--manifest-name',
dest='manifest_name',
help='initial manifest file', metavar='NAME.xml')
group.add_option('-c', '--current-branch',
group.add_option('--current-branch',
dest='current_branch_only', action='store_true',
help='fetch only current manifest branch from server')
group.add_option('--mirror',
@ -196,6 +237,13 @@ group.add_option('--dissociate',
group.add_option('--depth', type='int', default=None,
dest='depth',
help='create a shallow clone with given depth; see git clone')
group.add_option('--partial-clone', action='store_true',
dest='partial_clone',
help='perform partial clone (https://git-scm.com/'
'docs/gitrepository-layout#_code_partialclone_code)')
group.add_option('--clone-filter', action='store', default='blob:none',
dest='clone_filter',
help='filter for use with --partial-clone [default: %default]')
group.add_option('--archive',
dest='archive', action='store_true',
help='checkout an archive instead of a git repository for '
@ -225,10 +273,10 @@ group.add_option('--no-tags',
group = init_optparse.add_option_group('repo Version options')
group.add_option('--repo-url',
dest='repo_url',
help='repo repository location', metavar='URL')
help='repo repository location ($REPO_URL)', metavar='URL')
group.add_option('--repo-branch',
dest='repo_branch',
help='repo branch or revision', metavar='REVISION')
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')
@ -320,21 +368,21 @@ def _Init(args, gitc_init=False):
if branch.startswith('refs/heads/'):
branch = branch[len('refs/heads/'):]
if branch.startswith('refs/'):
_print("fatal: invalid branch name '%s'" % branch, file=sys.stderr)
print("fatal: invalid branch name '%s'" % branch, file=sys.stderr)
raise CloneFailure()
try:
if gitc_init:
gitc_manifest_dir = get_gitc_manifest_dir()
if not gitc_manifest_dir:
_print('fatal: GITC filesystem is not available. Exiting...',
file=sys.stderr)
print('fatal: GITC filesystem is not available. Exiting...',
file=sys.stderr)
sys.exit(1)
gitc_client = opt.gitc_client
if not gitc_client:
gitc_client = gitc_parse_clientdir(os.getcwd())
if not gitc_client:
_print('fatal: GITC client (-c) is required.', file=sys.stderr)
print('fatal: GITC client (-c) is required.', file=sys.stderr)
sys.exit(1)
client_dir = os.path.join(gitc_manifest_dir, gitc_client)
if not os.path.exists(client_dir):
@ -347,8 +395,8 @@ def _Init(args, gitc_init=False):
os.mkdir(repodir)
except OSError as e:
if e.errno != errno.EEXIST:
_print('fatal: cannot make %s directory: %s'
% (repodir, e.strerror), file=sys.stderr)
print('fatal: cannot make %s directory: %s'
% (repodir, e.strerror), file=sys.stderr)
# Don't raise CloneFailure; that would delete the
# name. Instead exit immediately.
#
@ -356,15 +404,18 @@ def _Init(args, gitc_init=False):
_CheckGitVersion()
try:
if NeedSetupGnuPG():
can_verify = SetupGnuPG(opt.quiet)
if opt.no_repo_verify:
do_verify = False
else:
can_verify = True
if NeedSetupGnuPG():
do_verify = SetupGnuPG(opt.quiet)
else:
do_verify = True
dst = os.path.abspath(os.path.join(repodir, S_repo))
_Clone(url, dst, opt.quiet, not opt.no_clone_bundle)
if can_verify and not opt.no_repo_verify:
if do_verify:
rev = _Verify(dst, branch, opt.quiet)
else:
rev = 'refs/remotes/origin/%s^0' % branch
@ -372,55 +423,73 @@ def _Init(args, gitc_init=False):
_Checkout(dst, branch, rev, opt.quiet)
if not os.path.isfile(os.path.join(dst, 'repo')):
_print("warning: '%s' does not look like a git-repo repository, is "
"REPO_URL set correctly?" % url, file=sys.stderr)
print("warning: '%s' does not look like a git-repo repository, is "
"REPO_URL set correctly?" % url, file=sys.stderr)
except CloneFailure:
if opt.quiet:
_print('fatal: repo init failed; run without --quiet to see why',
file=sys.stderr)
print('fatal: repo init failed; run without --quiet to see why',
file=sys.stderr)
raise
def ParseGitVersion(ver_str):
# The git version info broken down into components for easy analysis.
# Similar to Python's sys.version_info.
GitVersion = collections.namedtuple(
'GitVersion', ('major', 'minor', 'micro', 'full'))
def ParseGitVersion(ver_str=None):
if ver_str is None:
# Load the version ourselves.
ver_str = _GetGitVersion()
if not ver_str.startswith('git version '):
return None
num_ver_str = ver_str[len('git version '):].strip().split('-')[0]
full_version = ver_str[len('git version '):].strip()
num_ver_str = full_version.split('-')[0]
to_tuple = []
for num_str in num_ver_str.split('.')[:3]:
if num_str.isdigit():
to_tuple.append(int(num_str))
else:
to_tuple.append(0)
return tuple(to_tuple)
to_tuple.append(full_version)
return GitVersion(*to_tuple)
def _CheckGitVersion():
def _GetGitVersion():
cmd = [GIT, '--version']
try:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
except OSError as e:
_print(file=sys.stderr)
_print("fatal: '%s' is not available" % GIT, file=sys.stderr)
_print('fatal: %s' % e, file=sys.stderr)
_print(file=sys.stderr)
_print('Please make sure %s is installed and in your path.' % GIT,
file=sys.stderr)
raise CloneFailure()
print(file=sys.stderr)
print("fatal: '%s' is not available" % GIT, file=sys.stderr)
print('fatal: %s' % e, file=sys.stderr)
print(file=sys.stderr)
print('Please make sure %s is installed and in your path.' % GIT,
file=sys.stderr)
raise
ver_str = proc.stdout.read().strip()
proc.stdout.close()
proc.wait()
return ver_str.decode('utf-8')
def _CheckGitVersion():
try:
ver_act = ParseGitVersion()
except OSError:
raise CloneFailure()
ver_act = ParseGitVersion(ver_str)
if ver_act is None:
_print('error: "%s" unsupported' % ver_str, file=sys.stderr)
print('fatal: unable to detect git version', file=sys.stderr)
raise CloneFailure()
if ver_act < MIN_GIT_VERSION:
need = '.'.join(map(str, MIN_GIT_VERSION))
_print('fatal: git %s or later required' % need, file=sys.stderr)
print('fatal: git %s or later required' % need, file=sys.stderr)
raise CloneFailure()
@ -447,16 +516,16 @@ def SetupGnuPG(quiet):
os.mkdir(home_dot_repo)
except OSError as e:
if e.errno != errno.EEXIST:
_print('fatal: cannot make %s directory: %s'
% (home_dot_repo, e.strerror), file=sys.stderr)
print('fatal: cannot make %s directory: %s'
% (home_dot_repo, e.strerror), file=sys.stderr)
sys.exit(1)
try:
os.mkdir(gpg_dir, stat.S_IRWXU)
except OSError as e:
if e.errno != errno.EEXIST:
_print('fatal: cannot make %s directory: %s' % (gpg_dir, e.strerror),
file=sys.stderr)
print('fatal: cannot make %s directory: %s' % (gpg_dir, e.strerror),
file=sys.stderr)
sys.exit(1)
env = os.environ.copy()
@ -472,22 +541,21 @@ def SetupGnuPG(quiet):
stdin=subprocess.PIPE)
except OSError as e:
if not quiet:
_print('warning: gpg (GnuPG) is not available.', file=sys.stderr)
_print('warning: Installing it is strongly encouraged.', file=sys.stderr)
_print(file=sys.stderr)
print('warning: gpg (GnuPG) is not available.', file=sys.stderr)
print('warning: Installing it is strongly encouraged.', file=sys.stderr)
print(file=sys.stderr)
return False
proc.stdin.write(MAINTAINER_KEYS)
proc.stdin.write(MAINTAINER_KEYS.encode('utf-8'))
proc.stdin.close()
if proc.wait() != 0:
_print('fatal: registering repo maintainer keys failed', file=sys.stderr)
print('fatal: registering repo maintainer keys failed', file=sys.stderr)
sys.exit(1)
_print()
print()
fd = open(os.path.join(home_dot_repo, 'keyring-version'), 'w')
fd.write('.'.join(map(str, KEYRING_VERSION)) + '\n')
fd.close()
with open(os.path.join(home_dot_repo, 'keyring-version'), 'w') as fd:
fd.write('.'.join(map(str, KEYRING_VERSION)) + '\n')
return True
@ -526,7 +594,7 @@ def _InitHttp():
def _Fetch(url, local, src, quiet):
if not quiet:
_print('Get %s' % url, file=sys.stderr)
print('Get %s' % url, file=sys.stderr)
cmd = [GIT, 'fetch']
if quiet:
@ -556,6 +624,7 @@ def _DownloadBundle(url, local, quiet):
cwd=local,
stdout=subprocess.PIPE)
for line in proc.stdout:
line = line.decode('utf-8')
m = re.compile(r'^url\.(.*)\.insteadof (.*)$').match(line)
if m:
new_url = m.group(1)
@ -576,19 +645,19 @@ def _DownloadBundle(url, local, quiet):
except urllib.error.HTTPError as e:
if e.code in [401, 403, 404, 501]:
return False
_print('fatal: Cannot get %s' % url, file=sys.stderr)
_print('fatal: HTTP error %s' % e.code, file=sys.stderr)
print('fatal: Cannot get %s' % url, file=sys.stderr)
print('fatal: HTTP error %s' % e.code, file=sys.stderr)
raise CloneFailure()
except urllib.error.URLError as e:
_print('fatal: Cannot get %s' % url, file=sys.stderr)
_print('fatal: error %s' % e.reason, file=sys.stderr)
print('fatal: Cannot get %s' % url, file=sys.stderr)
print('fatal: error %s' % e.reason, file=sys.stderr)
raise CloneFailure()
try:
if not quiet:
_print('Get %s' % url, file=sys.stderr)
print('Get %s' % url, file=sys.stderr)
while True:
buf = r.read(8192)
if buf == '':
if not buf:
return True
dest.write(buf)
finally:
@ -611,23 +680,23 @@ def _Clone(url, local, quiet, clone_bundle):
try:
os.mkdir(local)
except OSError as e:
_print('fatal: cannot make %s directory: %s' % (local, e.strerror),
file=sys.stderr)
print('fatal: cannot make %s directory: %s' % (local, e.strerror),
file=sys.stderr)
raise CloneFailure()
cmd = [GIT, 'init', '--quiet']
try:
proc = subprocess.Popen(cmd, cwd=local)
except OSError as e:
_print(file=sys.stderr)
_print("fatal: '%s' is not available" % GIT, file=sys.stderr)
_print('fatal: %s' % e, file=sys.stderr)
_print(file=sys.stderr)
_print('Please make sure %s is installed and in your path.' % GIT,
file=sys.stderr)
print(file=sys.stderr)
print("fatal: '%s' is not available" % GIT, file=sys.stderr)
print('fatal: %s' % e, file=sys.stderr)
print(file=sys.stderr)
print('Please make sure %s is installed and in your path.' % GIT,
file=sys.stderr)
raise CloneFailure()
if proc.wait() != 0:
_print('fatal: could not create %s' % local, file=sys.stderr)
print('fatal: could not create %s' % local, file=sys.stderr)
raise CloneFailure()
_InitHttp()
@ -648,25 +717,25 @@ def _Verify(cwd, branch, quiet):
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=cwd)
cur = proc.stdout.read().strip()
cur = proc.stdout.read().strip().decode('utf-8')
proc.stdout.close()
proc.stderr.read()
proc.stderr.close()
if proc.wait() != 0 or not cur:
_print(file=sys.stderr)
_print("fatal: branch '%s' has not been signed" % branch, file=sys.stderr)
print(file=sys.stderr)
print("fatal: branch '%s' has not been signed" % branch, file=sys.stderr)
raise CloneFailure()
m = re.compile(r'^(.*)-[0-9]{1,}-g[0-9a-f]{1,}$').match(cur)
if m:
cur = m.group(1)
if not quiet:
_print(file=sys.stderr)
_print("info: Ignoring branch '%s'; using tagged release '%s'"
% (branch, cur), file=sys.stderr)
_print(file=sys.stderr)
print(file=sys.stderr)
print("info: Ignoring branch '%s'; using tagged release '%s'"
% (branch, cur), file=sys.stderr)
print(file=sys.stderr)
env = os.environ.copy()
try:
@ -680,17 +749,17 @@ def _Verify(cwd, branch, quiet):
stderr=subprocess.PIPE,
cwd=cwd,
env=env)
out = proc.stdout.read()
out = proc.stdout.read().decode('utf-8')
proc.stdout.close()
err = proc.stderr.read()
err = proc.stderr.read().decode('utf-8')
proc.stderr.close()
if proc.wait() != 0:
_print(file=sys.stderr)
_print(out, file=sys.stderr)
_print(err, file=sys.stderr)
_print(file=sys.stderr)
print(file=sys.stderr)
print(out, file=sys.stderr)
print(err, file=sys.stderr)
print(file=sys.stderr)
raise CloneFailure()
return '%s^0' % cur
@ -761,7 +830,7 @@ def _Usage():
if get_gitc_manifest_dir():
gitc_usage = " gitc-init Initialize a GITC Client.\n"
_print(
print(
"""usage: repo COMMAND [ARGS]
repo is not yet installed. Use "repo init" to install it here.
@ -773,8 +842,8 @@ The most commonly used repo commands are:
""" help Display detailed help on a command
For access to the full online help, install repo ("repo init").
""", file=sys.stderr)
sys.exit(1)
""")
sys.exit(0)
def _Help(args):
@ -787,23 +856,23 @@ def _Help(args):
init_optparse.print_help()
sys.exit(0)
else:
_print("error: '%s' is not a bootstrap command.\n"
' For access to online help, install repo ("repo init").'
% args[0], file=sys.stderr)
print("error: '%s' is not a bootstrap command.\n"
' For access to online help, install repo ("repo init").'
% args[0], file=sys.stderr)
else:
_Usage()
sys.exit(1)
def _NotInstalled():
_print('error: repo is not installed. Use "repo init" to install it here.',
file=sys.stderr)
print('error: repo is not installed. Use "repo init" to install it here.',
file=sys.stderr)
sys.exit(1)
def _NoCommands(cmd):
_print("""error: command '%s' requires repo to be installed first.
Use "repo init" to install it here.""" % cmd, file=sys.stderr)
print("""error: command '%s' requires repo to be installed first.
Use "repo init" to install it here.""" % cmd, file=sys.stderr)
sys.exit(1)
@ -833,14 +902,14 @@ def _SetDefaultsTo(gitdir):
'HEAD'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
REPO_REV = proc.stdout.read().strip()
REPO_REV = proc.stdout.read().strip().decode('utf-8')
proc.stdout.close()
proc.stderr.read()
proc.stderr.close()
if proc.wait() != 0:
_print('fatal: %s has no current branch' % gitdir, file=sys.stderr)
print('fatal: %s has no current branch' % gitdir, file=sys.stderr)
sys.exit(1)
@ -857,10 +926,10 @@ def main(orig_args):
cwd = os.getcwd()
if get_gitc_manifest_dir() and cwd.startswith(get_gitc_manifest_dir()):
_print('error: repo cannot be used in the GITC local manifest directory.'
'\nIf you want to work on this GITC client please rerun this '
'command from the corresponding client under /gitc/',
file=sys.stderr)
print('error: repo cannot be used in the GITC local manifest directory.'
'\nIf you want to work on this GITC client please rerun this '
'command from the corresponding client under /gitc/',
file=sys.stderr)
sys.exit(1)
if not repo_main:
if opt.help:
@ -876,8 +945,8 @@ def main(orig_args):
_Init(args, gitc_init=(cmd == 'gitc-init'))
except CloneFailure:
path = os.path.join(repodir, S_repo)
_print("fatal: cloning the git-repo repository failed, will remove "
"'%s' " % path, file=sys.stderr)
print("fatal: cloning the git-repo repository failed, will remove "
"'%s' " % path, file=sys.stderr)
shutil.rmtree(path, ignore_errors=True)
sys.exit(1)
repo_main, rel_repo_dir = _FindRepo()
@ -895,20 +964,10 @@ def main(orig_args):
'--']
me.extend(orig_args)
me.extend(extra_args)
try:
if platform.system() == "Windows":
sys.exit(subprocess.call(me))
else:
os.execv(sys.executable, me)
except OSError as e:
_print("fatal: unable to start %s" % repo_main, file=sys.stderr)
_print("fatal: %s" % e, file=sys.stderr)
sys.exit(148)
exec_command(me)
print("fatal: unable to start %s" % repo_main, file=sys.stderr)
sys.exit(148)
if __name__ == '__main__':
if ver[0] == 3:
_print('warning: Python 3 support is currently experimental. YMMV.\n'
'Please use Python 2.6 - 2.7 instead.',
file=sys.stderr)
main(sys.argv[1:])

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#
@ -13,15 +14,19 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Logic for tracing repo interactions.
Activated via `repo --trace ...` or `REPO_TRACE=1 repo ...`.
"""
from __future__ import print_function
import sys
import os
# Env var to implicitly turn on tracing.
REPO_TRACE = 'REPO_TRACE'
try:
_TRACE = os.environ[REPO_TRACE] == '1'
except KeyError:
_TRACE = False
_TRACE = os.environ.get(REPO_TRACE) == '1'
def IsTrace():
return _TRACE

53
run_tests Executable file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Copyright 2019 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Wrapper to run pytest with the right settings."""
from __future__ import print_function
import errno
import os
import subprocess
import sys
def run_pytest(cmd, argv):
"""Run the unittests via |cmd|."""
try:
return subprocess.call([cmd] + argv)
except OSError as e:
if e.errno == errno.ENOENT:
print('%s: unable to run `%s`: %s' % (__file__, cmd, e), file=sys.stderr)
print('%s: Try installing pytest: sudo apt-get install python-pytest' %
(__file__,), file=sys.stderr)
return 127
else:
raise
def main(argv):
"""The main entry."""
# Add the repo tree to PYTHONPATH as the tests expect to be able to import
# modules directly.
topdir = os.path.dirname(os.path.realpath(__file__))
pythonpath = os.environ.get('PYTHONPATH', '')
os.environ['PYTHONPATH'] = '%s:%s' % (topdir, pythonpath)
return run_pytest('pytest', argv)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))

63
setup.py Executable file
View File

@ -0,0 +1,63 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Copyright 2019 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the 'License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Python packaging for repo."""
from __future__ import print_function
import os
import setuptools
TOPDIR = os.path.dirname(os.path.abspath(__file__))
# Rip out the first intro paragraph.
with open(os.path.join(TOPDIR, 'README.md')) as fp:
lines = fp.read().splitlines()[2:]
end = lines.index('')
long_description = ' '.join(lines[0:end])
# https://packaging.python.org/tutorials/packaging-projects/
setuptools.setup(
name='repo',
version='1.13.8',
maintainer='Various',
maintainer_email='repo-discuss@googlegroups.com',
description='Repo helps manage many Git repositories',
long_description=long_description,
long_description_content_type='text/plain',
url='https://gerrit.googlesource.com/git-repo/',
project_urls={
'Bug Tracker': 'https://bugs.chromium.org/p/gerrit/issues/list?q=component:repo',
},
# https://pypi.org/classifiers/
classifiers=[
'Development Status :: 6 - Mature',
'Environment :: Console',
'Intended Audience :: Developers',
'License :: OSI Approved :: Apache Software License',
'Natural Language :: English',
'Operating System :: MacOS :: MacOS X',
'Operating System :: Microsoft :: Windows :: Windows 10',
'Operating System :: POSIX :: Linux',
'Topic :: Software Development :: Version Control :: Git',
],
# We support Python 2.7 and Python 3.6+.
python_requires='>=2.7, ' + ', '.join('!=3.%i.*' % x for x in range(0, 6)),
packages=['subcmds'],
)

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#
@ -36,19 +37,19 @@ It is equivalent to "git branch -D <branchname>".
dest='all', action='store_true',
help='delete all branches in all projects')
def Execute(self, opt, args):
def ValidateOptions(self, opt, args):
if not opt.all and not args:
self.Usage()
if not opt.all:
nb = args[0]
if not git.check_ref_format('heads/%s' % nb):
print("error: '%s' is not a valid name" % nb, file=sys.stderr)
sys.exit(1)
self.OptionParser.error("'%s' is not a valid branch name" % nb)
else:
args.insert(0,None)
nb = "'All local branches'"
args.insert(0, "'All local branches'")
def Execute(self, opt, args):
nb = args[0]
err = defaultdict(list)
success = defaultdict(list)
all_projects = self.GetProjects(args[1:])
@ -58,7 +59,7 @@ It is equivalent to "git branch -D <branchname>".
pm.update()
if opt.all:
branches = project.GetBranches().keys()
branches = list(project.GetBranches().keys())
else:
branches = [nb]

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2009 The Android Open Source Project
#

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2009 The Android Open Source Project
#
@ -33,10 +34,11 @@ The command is equivalent to:
repo forall [<project>...] -c git checkout <branchname>
"""
def Execute(self, opt, args):
def ValidateOptions(self, opt, args):
if not args:
self.Usage()
def Execute(self, opt, args):
nb = args[0]
err = []
success = []

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2010 The Android Open Source Project
#
@ -36,10 +37,11 @@ change id will be added.
def _Options(self, p):
pass
def Execute(self, opt, args):
def ValidateOptions(self, opt, args):
if len(args) != 1:
self.Usage()
def Execute(self, opt, args):
reference = args[0]
p = GitCommand(None,

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#
@ -36,5 +37,8 @@ to the Unix 'patch' command.
help='Paths are relative to the repository root')
def Execute(self, opt, args):
ret = 0
for project in self.GetProjects(args):
project.PrintWorkTreeDiff(opt.absolute)
if not project.PrintWorkTreeDiff(opt.absolute):
ret = 1
return ret

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2014 The Android Open Source Project
#
@ -175,10 +176,11 @@ synced and their revisions won't be found.
self.printText(log)
self.out.nl()
def Execute(self, opt, args):
def ValidateOptions(self, opt, args):
if not args or len(args) > 2:
self.Usage()
self.OptionParser.error('missing manifests to diff')
def Execute(self, opt, args):
self.out = _Coloring(self.manifest.globalConfig)
self.printText = self.out.nofmt_printer('text')
if opt.color:
@ -190,12 +192,12 @@ synced and their revisions won't be found.
self.printProject = self.printAdded = self.printRemoved = self.printRevision = self.printText
manifest1 = XmlManifest(self.manifest.repodir)
manifest1.Override(args[0])
manifest1.Override(args[0], load_local_manifests=False)
if len(args) == 1:
manifest2 = self.manifest
else:
manifest2 = XmlManifest(self.manifest.repodir)
manifest2.Override(args[1])
manifest2.Override(args[1], load_local_manifests=False)
diff = manifest1.projectsDiff(manifest2)
if opt.raw:

1
subcmds/download.py Executable file → Normal file
View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#
@ -104,7 +105,7 @@ following <command>.
Example: to list projects:
%prog% forall -c 'echo $REPO_PROJECT'
%prog -c 'echo $REPO_PROJECT'
Notice that $REPO_PROJECT is quoted to ensure it is expanded in
the context of running <command> instead of in the calling shell.
@ -138,6 +139,9 @@ without iterating through the remaining projects.
p.add_option('-e', '--abort-on-errors',
dest='abort_on_errors', action='store_true',
help='Abort if a command exits unsuccessfully')
p.add_option('--ignore-missing', action='store_true',
help='Silently skip & do not exit non-zero due missing '
'checkouts')
g = p.add_option_group('Output')
g.add_option('-p',
@ -176,10 +180,11 @@ without iterating through the remaining projects.
'worktree': project.worktree,
}
def Execute(self, opt, args):
def ValidateOptions(self, opt, args):
if not opt.command:
self.Usage()
def Execute(self, opt, args):
cmd = [opt.command[0]]
shell = True
@ -321,10 +326,14 @@ def DoWork(project, mirror, opt, cmd, shell, cnt, config):
cwd = project['worktree']
if not os.path.exists(cwd):
if (opt.project_header and opt.verbose) \
or not opt.project_header:
# Allow the user to silently ignore missing checkouts so they can run on
# partial checkouts (good for infra recovery tools).
if opt.ignore_missing:
return 0
if ((opt.project_header and opt.verbose)
or not opt.project_header):
print('skipping %s/' % project['relpath'], file=sys.stderr)
return
return 1
if opt.project_header:
stdin = subprocess.PIPE
@ -357,7 +366,7 @@ def DoWork(project, mirror, opt, cmd, shell, cnt, config):
while not s_in.is_done:
in_ready = s_in.select()
for s in in_ready:
buf = s.read()
buf = s.read().decode()
if not buf:
s.close()
s_in.remove(s)

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2015 The Android Open Source Project
#

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2015 The Android Open Source Project
#
@ -49,7 +50,7 @@ use for this GITC client.
"""
def _Options(self, p):
super(GitcInit, self)._Options(p)
super(GitcInit, self)._Options(p, gitc_init=True)
g = p.add_option_group('GITC options')
g.add_option('-f', '--manifest-file',
dest='manifest_file',

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2009 The Android Open Source Project
#
@ -14,15 +15,19 @@
# limitations under the License.
from __future__ import print_function
import sys
from color import Coloring
from command import PagedCommand
from error import GitError
from git_command import git_require, GitCommand
class GrepColoring(Coloring):
def __init__(self, config):
Coloring.__init__(self, config, 'grep')
self.project = self.printer('project', attr='bold')
self.fail = self.printer('fail', fg='red')
class Grep(PagedCommand):
common = True
@ -183,15 +188,25 @@ contain a line that matches both expressions:
cmd_argv.extend(opt.revision)
cmd_argv.append('--')
git_failed = False
bad_rev = False
have_match = False
for project in projects:
p = GitCommand(project,
cmd_argv,
bare = False,
capture_stdout = True,
capture_stderr = True)
try:
p = GitCommand(project,
cmd_argv,
bare=False,
capture_stdout=True,
capture_stderr=True)
except GitError as e:
git_failed = True
out.project('--- project %s ---' % project.relpath)
out.nl()
out.fail('%s', str(e))
out.nl()
continue
if p.Wait() != 0:
# no results
#
@ -201,7 +216,7 @@ contain a line that matches both expressions:
else:
out.project('--- project %s ---' % project.relpath)
out.nl()
out.write("%s", p.stderr)
out.fail('%s', p.stderr.strip())
out.nl()
continue
have_match = True
@ -230,7 +245,9 @@ contain a line that matches both expressions:
for line in r:
print(line)
if have_match:
if git_failed:
sys.exit(1)
elif have_match:
sys.exit(0)
elif have_rev and bad_rev:
for r in opt.revision:

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#
@ -32,11 +33,8 @@ class Help(PagedCommand, MirrorSafeCommand):
Displays detailed usage information about a command.
"""
def _PrintAllCommands(self):
print('usage: repo COMMAND [ARGS]')
print('The complete list of recognized repo commands are:')
commandNames = list(sorted(self.commands))
def _PrintCommands(self, commandNames):
"""Helper to display |commandNames| summaries."""
maxlen = 0
for name in commandNames:
maxlen = max(maxlen, len(name))
@ -49,6 +47,12 @@ Displays detailed usage information about a command.
except AttributeError:
summary = ''
print(fmt % (name, summary))
def _PrintAllCommands(self):
print('usage: repo COMMAND [ARGS]')
print('The complete list of recognized repo commands are:')
commandNames = list(sorted(self.commands))
self._PrintCommands(commandNames)
print("See 'repo help <command>' for more information on a "
'specific command.')
@ -70,24 +74,13 @@ Displays detailed usage information about a command.
commandNames = list(sorted([name
for name, command in self.commands.items()
if command.common and gitc_supported(command)]))
self._PrintCommands(commandNames)
maxlen = 0
for name in commandNames:
maxlen = max(maxlen, len(name))
fmt = ' %%-%ds %%s' % maxlen
for name in commandNames:
command = self.commands[name]
try:
summary = command.helpSummary.strip()
except AttributeError:
summary = ''
print(fmt % (name, summary))
print(
"See 'repo help <command>' for more information on a specific command.\n"
"See 'repo help --all' for a complete list of recognized commands.")
def _PrintCommandHelp(self, cmd):
def _PrintCommandHelp(self, cmd, header_prefix=''):
class _Out(Coloring):
def __init__(self, gc):
Coloring.__init__(self, gc, 'help')
@ -105,7 +98,7 @@ Displays detailed usage information about a command.
self.nl()
self.heading('%s', heading)
self.heading('%s%s', header_prefix, heading)
self.nl()
self.nl()
@ -123,7 +116,7 @@ Displays detailed usage information about a command.
m = asciidoc_hdr.match(para)
if m:
self.heading(m.group(1))
self.heading('%s%s', header_prefix, m.group(1))
self.nl()
self.nl()
continue
@ -137,14 +130,25 @@ Displays detailed usage information about a command.
cmd.OptionParser.print_help()
out._PrintSection('Description', 'helpDescription')
def _PrintAllCommandHelp(self):
for name in sorted(self.commands):
cmd = self.commands[name]
cmd.manifest = self.manifest
self._PrintCommandHelp(cmd, header_prefix='[%s] ' % (name,))
def _Options(self, p):
p.add_option('-a', '--all',
dest='show_all', action='store_true',
help='show the complete list of commands')
p.add_option('--help-all',
dest='show_all_help', action='store_true',
help='show the --help of all commands')
def Execute(self, opt, args):
if len(args) == 0:
if opt.show_all:
if opt.show_all_help:
self._PrintAllCommandHelp()
elif opt.show_all:
self._PrintAllCommands()
else:
self._PrintCommonCommands()

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2012 The Android Open Source Project
#
@ -15,7 +16,6 @@
from command import PagedCommand
from color import Coloring
from error import NoSuchProjectError
from git_refs import R_M
class _Coloring(Coloring):
@ -45,7 +45,7 @@ class Info(PagedCommand):
def Execute(self, opt, args):
self.out = _Coloring(self.manifest.globalConfig)
self.heading = self.out.printer('heading', attr = 'bold')
self.headtext = self.out.printer('headtext', fg = 'yellow')
self.headtext = self.out.nofmt_printer('headtext', fg = 'yellow')
self.redtext = self.out.printer('redtext', fg = 'red')
self.sha = self.out.printer("sha", fg = 'yellow')
self.text = self.out.nofmt_printer('text')
@ -81,10 +81,8 @@ class Info(PagedCommand):
self.out.nl()
def printDiffInfo(self, args):
try:
projs = self.GetProjects(args)
except NoSuchProjectError:
return
# We let exceptions bubble up to main as they'll be well structured.
projs = self.GetProjects(args)
for p in projs:
self.heading("Project: ")
@ -96,13 +94,23 @@ class Info(PagedCommand):
self.out.nl()
self.heading("Current revision: ")
self.headtext(p.GetRevisionId())
self.out.nl()
currentBranch = p.CurrentBranch
if currentBranch:
self.heading('Current branch: ')
self.headtext(currentBranch)
self.out.nl()
self.heading("Manifest revision: ")
self.headtext(p.revisionExpr)
self.out.nl()
localBranches = p.GetBranches().keys()
localBranches = list(p.GetBranches().keys())
self.heading("Local Branches: ")
self.redtext(str(len(localBranches)))
if len(localBranches) > 0:
if localBranches:
self.text(" [")
self.text(", ".join(localBranches))
self.text("]")

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#
@ -33,7 +34,7 @@ from command import InteractiveCommand, MirrorSafeCommand
from error import ManifestParseError
from project import SyncBuffer
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
class Init(InteractiveCommand, MirrorSafeCommand):
@ -80,7 +81,7 @@ manifest, a subsequent `repo sync` (or `repo sync -d`) is necessary
to update the working directory files.
"""
def _Options(self, p):
def _Options(self, p, gitc_init=False):
# Logging
g = p.add_option_group('Logging options')
g.add_option('-q', '--quiet',
@ -95,7 +96,12 @@ to update the working directory files.
g.add_option('-b', '--manifest-branch',
dest='manifest_branch',
help='manifest branch or revision', metavar='REVISION')
g.add_option('-c', '--current-branch',
cbr_opts = ['--current-branch']
# The gitc-init subcommand allocates -c itself, but a lot of init users
# want -c, so try to satisfy both as best we can.
if not gitc_init:
cbr_opts += ['-c']
g.add_option(*cbr_opts,
dest='current_branch_only', action='store_true',
help='fetch only current manifest branch from server')
g.add_option('-m', '--manifest-name',
@ -114,6 +120,13 @@ to update the working directory files.
g.add_option('--depth', type='int', default=None,
dest='depth',
help='create a shallow clone with given depth; see git clone')
g.add_option('--partial-clone', action='store_true',
dest='partial_clone',
help='perform partial clone (https://git-scm.com/'
'docs/gitrepository-layout#_code_partialclone_code)')
g.add_option('--clone-filter', action='store', default='blob:none',
dest='clone_filter',
help='filter for use with --partial-clone [default: %default]')
g.add_option('--archive',
dest='archive', action='store_true',
help='checkout an archive instead of a git repository for '
@ -197,6 +210,8 @@ to update the working directory files.
else:
m.PreSync()
self._ConfigureDepth(opt)
if opt.manifest_url:
r = m.GetRemote(m.remote.name)
r.url = opt.manifest_url
@ -250,13 +265,25 @@ to update the working directory files.
'in another location.', file=sys.stderr)
sys.exit(1)
if opt.partial_clone:
if opt.mirror:
print('fatal: --mirror and --partial-clone are mutually exclusive',
file=sys.stderr)
sys.exit(1)
m.config.SetString('repo.partialclone', 'true')
if opt.clone_filter:
m.config.SetString('repo.clonefilter', opt.clone_filter)
else:
opt.clone_filter = None
if opt.submodules:
m.config.SetString('repo.submodules', 'true')
if not m.Sync_NetworkHalf(is_new=is_new, quiet=opt.quiet,
clone_bundle=not opt.no_clone_bundle,
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):
r = m.GetRemote(m.remote.name)
print('fatal: cannot obtain manifest %s' % r.url, file=sys.stderr)
@ -291,7 +318,9 @@ to update the working directory files.
sys.exit(1)
def _Prompt(self, prompt, value):
sys.stdout.write('%-10s [%s]: ' % (prompt, value))
print('%-10s [%s]: ' % (prompt, value), end='')
# TODO: When we require Python 3, use flush=True w/print above.
sys.stdout.flush()
a = sys.stdin.readline().strip()
if a == '':
return value
@ -325,7 +354,9 @@ to update the working directory files.
print()
print('Your identity is: %s <%s>' % (name, email))
sys.stdout.write('is this correct [y/N]? ')
print('is this correct [y/N]? ', end='')
# TODO: When we require Python 3, use flush=True w/print above.
sys.stdout.flush()
a = sys.stdin.readline().strip().lower()
if a in ('yes', 'y', 't', 'true'):
break
@ -367,7 +398,9 @@ to update the working directory files.
out.printer(fg='black', attr=c)(' %-6s ', c)
out.nl()
sys.stdout.write('Enable color display in this user account (y/N)? ')
print('Enable color display in this user account (y/N)? ', end='')
# TODO: When we require Python 3, use flush=True w/print above.
sys.stdout.flush()
a = sys.stdin.readline().strip().lower()
if a in ('y', 'yes', 't', 'true', 'on'):
gc.SetString('color.ui', 'auto')
@ -408,18 +441,22 @@ to update the working directory files.
print(' rm -r %s/.repo' % self.manifest.topdir)
print('and try again.')
def Execute(self, opt, args):
git_require(MIN_GIT_VERSION, fail=True)
def ValidateOptions(self, opt, args):
if opt.reference:
opt.reference = os.path.expanduser(opt.reference)
# Check this here, else manifest will be tagged "not new" and init won't be
# possible anymore without removing the .repo/manifests directory.
if opt.archive and opt.mirror:
print('fatal: --mirror and --archive cannot be used together.',
self.OptionParser.error('--mirror and --archive cannot be used together.')
def Execute(self, opt, args):
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)
sys.exit(1)
self._SyncManifest(opt)
self._LinkManifest(opt.manifest_name)
@ -429,6 +466,4 @@ to update the working directory files.
self._ConfigureUser()
self._ConfigureColor()
self._ConfigureDepth(opt)
self._DisplayResult()

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2011 The Android Open Source Project
#
@ -48,6 +49,10 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
dest='path_only', action='store_true',
help="Display only the path of the repository")
def ValidateOptions(self, opt, args):
if opt.fullpath and opt.name_only:
self.OptionParser.error('cannot combine -f and -n')
def Execute(self, opt, args):
"""List all projects and the associated directories.
@ -59,11 +64,6 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
opt: The options.
args: Positional args. Can be a list of projects to list, or empty.
"""
if opt.fullpath and opt.name_only:
print('error: cannot combine -f and -n', file=sys.stderr)
sys.exit(1)
if not opt.regex:
projects = self.GetProjects(args, groups=opt.groups)
else:

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2009 The Android Open Source Project
#
@ -39,10 +40,9 @@ in a Git repository for use during future 'repo init' invocations.
helptext = self._helpDescription + '\n'
r = os.path.dirname(__file__)
r = os.path.dirname(r)
fd = open(os.path.join(r, 'docs', 'manifest-format.md'))
for line in fd:
helptext += line
fd.close()
with open(os.path.join(r, 'docs', 'manifest-format.md')) as fd:
for line in fd:
helptext += line
return helptext
def _Options(self, p):
@ -72,14 +72,9 @@ in a Git repository for use during future 'repo init' invocations.
if opt.output_file != '-':
print('Saved manifest to %s' % opt.output_file, file=sys.stderr)
def Execute(self, opt, args):
def ValidateOptions(self, opt, args):
if args:
self.Usage()
if opt.output_file is not None:
self._Output(opt)
return
print('error: no operation to perform', file=sys.stderr)
print('error: see repo help manifest', file=sys.stderr)
sys.exit(1)
def Execute(self, opt, args):
self._Output(opt)

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2012 The Android Open Source Project
#

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#
@ -50,11 +51,16 @@ class Prune(PagedCommand):
out.project('project %s/' % project.relpath)
out.nl()
commits = branch.commits
date = branch.date
print('%s %-33s (%2d commit%s, %s)' % (
print('%s %-33s ' % (
branch.name == project.CurrentBranch and '*' or ' ',
branch.name,
branch.name), end='')
if not branch.base_exists:
print('(ignoring: tracking branch is gone: %s)' % (branch.base,))
else:
commits = branch.commits
date = branch.date
print('(%2d commit%s, %s)' % (
len(commits),
len(commits) != 1 and 's' or ' ',
date))

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2010 The Android Open Source Project
#
@ -16,9 +17,18 @@
from __future__ import print_function
import sys
from color import Coloring
from command import Command
from git_command import GitCommand
class RebaseColoring(Coloring):
def __init__(self, config):
Coloring.__init__(self, config, 'rebase')
self.project = self.printer('project', attr='bold')
self.fail = self.printer('fail', fg='red')
class Rebase(Command):
common = True
helpSummary = "Rebase local branches on upstream branch"
@ -36,6 +46,9 @@ branch but need to incorporate new upstream changes "underneath" them.
dest="interactive", action="store_true",
help="interactive rebase (single project only)")
p.add_option('--fail-fast',
dest='fail_fast', action='store_true',
help='Stop rebasing after first error is hit')
p.add_option('-f', '--force-rebase',
dest='force_rebase', action='store_true',
help='Pass --force-rebase to git rebase')
@ -70,15 +83,38 @@ branch but need to incorporate new upstream changes "underneath" them.
if len(args) == 1:
print('note: project %s is mapped to more than one path' % (args[0],),
file=sys.stderr)
return -1
return 1
# Setup the common git rebase args that we use for all projects.
common_args = ['rebase']
if opt.whitespace:
common_args.append('--whitespace=%s' % opt.whitespace)
if opt.quiet:
common_args.append('--quiet')
if opt.force_rebase:
common_args.append('--force-rebase')
if opt.no_ff:
common_args.append('--no-ff')
if opt.autosquash:
common_args.append('--autosquash')
if opt.interactive:
common_args.append('-i')
config = self.manifest.manifestProject.config
out = RebaseColoring(config)
out.redirect(sys.stdout)
ret = 0
for project in all_projects:
if ret and opt.fail_fast:
break
cb = project.CurrentBranch
if not cb:
if one_project:
print("error: project %s has a detached HEAD" % project.relpath,
file=sys.stderr)
return -1
return 1
# ignore branches with detatched HEADs
continue
@ -87,38 +123,21 @@ branch but need to incorporate new upstream changes "underneath" them.
if one_project:
print("error: project %s does not track any remote branches"
% project.relpath, file=sys.stderr)
return -1
return 1
# ignore branches without remotes
continue
args = ["rebase"]
if opt.whitespace:
args.append('--whitespace=%s' % opt.whitespace)
if opt.quiet:
args.append('--quiet')
if opt.force_rebase:
args.append('--force-rebase')
if opt.no_ff:
args.append('--no-ff')
if opt.autosquash:
args.append('--autosquash')
if opt.interactive:
args.append("-i")
args = common_args[:]
if opt.onto_manifest:
args.append('--onto')
args.append(project.revisionExpr)
args.append(upbranch.LocalMerge)
print('# %s: rebasing %s -> %s'
% (project.relpath, cb, upbranch.LocalMerge), file=sys.stderr)
out.project('project %s: rebasing %s -> %s',
project.relpath, cb, upbranch.LocalMerge)
out.nl()
out.flush()
needs_stash = False
if opt.auto_stash:
@ -130,13 +149,21 @@ branch but need to incorporate new upstream changes "underneath" them.
stash_args = ["stash"]
if GitCommand(project, stash_args).Wait() != 0:
return -1
ret += 1
continue
if GitCommand(project, args).Wait() != 0:
return -1
ret += 1
continue
if needs_stash:
stash_args.append('pop')
stash_args.append('--quiet')
if GitCommand(project, stash_args).Wait() != 0:
return -1
ret += 1
if ret:
out.fail('%i projects had errors', ret)
out.nl()
return ret

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2009 The Android Open Source Project
#

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2010 The Android Open Source Project
#

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#
@ -39,16 +40,21 @@ revision specified in the manifest.
p.add_option('--all',
dest='all', action='store_true',
help='begin branch in all projects')
p.add_option('-r', '--rev', '--revision', dest='revision',
help='point branch at this revision instead of upstream')
p.add_option('--head', dest='revision', action='store_const', const='HEAD',
help='abbreviation for --rev HEAD')
def Execute(self, opt, args):
def ValidateOptions(self, opt, args):
if not args:
self.Usage()
nb = args[0]
if not git.check_ref_format('heads/%s' % nb):
print("error: '%s' is not a valid name" % nb, file=sys.stderr)
sys.exit(1)
self.OptionParser.error("'%s' is not a valid name" % nb)
def Execute(self, opt, args):
nb = args[0]
err = []
projects = []
if not opt.all:
@ -106,7 +112,8 @@ revision specified in the manifest.
else:
branch_merge = self.manifest.default.revisionExpr
if not project.StartBranch(nb, branch_merge=branch_merge):
if not project.StartBranch(
nb, branch_merge=branch_merge, revision=opt.revision):
err.append(project)
pm.end()

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#
@ -13,6 +14,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import print_function
from command import PagedCommand
try:

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#
@ -84,6 +85,9 @@ class _FetchError(Exception):
"""Internal error thrown in _FetchHelper() when we don't want stack trace."""
pass
class _CheckoutError(Exception):
"""Internal error thrown in _CheckoutOne() when we don't want stack trace."""
class Sync(Command, MirrorSafeCommand):
jobs = 1
common = True
@ -128,14 +132,19 @@ from the user's .netrc file.
if the manifest server specified in the manifest file already includes
credentials.
The -f/--force-broken option can be used to proceed with syncing
other projects if a project sync fails.
By default, all projects will be synced. The --fail-fast option can be used
to halt syncing as soon as possible when the the first project fails to sync.
The --force-sync option can be used to overwrite existing git
directories if they have previously been linked to a different
object direcotry. WARNING: This may cause data to be lost since
refs may be removed when overwriting.
The --force-remove-dirty option can be used to remove previously used
projects with uncommitted changes. WARNING: This may cause data to be
lost since uncommitted changes may be removed with projects that no longer
exist in the manifest.
The --no-clone-bundle option disables any attempt to use
$URL/clone.bundle to bootstrap a new Git repository from a
resumeable bundle file on a content delivery network. This
@ -191,15 +200,27 @@ later is required to fix a server side protocol bug.
p.add_option('-f', '--force-broken',
dest='force_broken', action='store_true',
help="continue sync even if a project fails to sync")
help='obsolete option (to be deleted in the future)')
p.add_option('--fail-fast',
dest='fail_fast', action='store_true',
help='stop syncing after first error is hit')
p.add_option('--force-sync',
dest='force_sync', action='store_true',
help="overwrite an existing git directory if it needs to "
"point to a different object directory. WARNING: this "
"may cause loss of data")
p.add_option('--force-remove-dirty',
dest='force_remove_dirty', action='store_true',
help="force remove projects with uncommitted modifications if "
"projects no longer exist in the manifest. "
"WARNING: this may cause loss of data")
p.add_option('-l', '--local-only',
dest='local_only', action='store_true',
help="only update working tree, don't fetch")
p.add_option('--no-manifest-update','--nmu',
dest='mp_update', action='store_false', default='true',
help='use the existing manifest checkout as-is. '
'(do not update to the latest revision)')
p.add_option('-n', '--network-only',
dest='network_only', action='store_true',
help="fetch only, don't update working tree")
@ -255,7 +276,7 @@ later is required to fix a server side protocol bug.
help=SUPPRESS_HELP)
def _FetchProjectList(self, opt, projects, sem, *args, **kwargs):
"""Main function of the fetch threads when jobs are > 1.
"""Main function of the fetch threads.
Delegates most of the work to _FetchHelper.
@ -270,12 +291,13 @@ later is required to fix a server side protocol bug.
try:
for project in projects:
success = self._FetchHelper(opt, project, *args, **kwargs)
if not success and not opt.force_broken:
if not success and opt.fail_fast:
break
finally:
sem.release()
def _FetchHelper(self, opt, project, lock, fetched, pm, err_event):
def _FetchHelper(self, opt, project, lock, fetched, pm, err_event,
clone_filter):
"""Fetch git objects for a single project.
Args:
@ -289,6 +311,7 @@ later is required to fix a server side protocol bug.
lock held).
err_event: We'll set this event in the case of an error (after printing
out info about the error).
clone_filter: Filter for use in a partial clone.
Returns:
Whether the fetch was successful.
@ -296,12 +319,8 @@ later is required to fix a server side protocol bug.
# We'll set to true once we've locked the lock.
did_lock = False
if not opt.quiet:
print('Fetching project %s' % project.name)
# Encapsulate everything in a try/except/finally so that:
# - We always set err_event in the case of an exception.
# - We always make sure we call sem.release().
# - We always make sure we unlock the lock if we locked it.
start = time.time()
success = False
@ -314,7 +333,8 @@ later is required to fix a server side protocol bug.
clone_bundle=not opt.no_clone_bundle,
no_tags=opt.no_tags, archive=self.manifest.IsArchive,
optimized_fetch=opt.optimized_fetch,
prune=opt.prune)
prune=opt.prune,
clone_filter=clone_filter)
self._fetch_times.Set(project, time.time() - start)
# Lock around all the rest of the code, since printing, updating a set
@ -327,14 +347,11 @@ later is required to fix a server side protocol bug.
print('error: Cannot fetch %s from %s'
% (project.name, project.remote.url),
file=sys.stderr)
if opt.force_broken:
print('warn: --force-broken, continuing to sync',
file=sys.stderr)
else:
if opt.fail_fast:
raise _FetchError()
fetched.add(project.gitdir)
pm.update()
pm.update(msg=project.name)
except _FetchError:
pass
except Exception as e:
@ -351,11 +368,10 @@ later is required to fix a server side protocol bug.
return success
def _Fetch(self, projects, opt):
def _Fetch(self, projects, opt, err_event):
fetched = set()
lock = _threading.Lock()
pm = Progress('Fetching projects', len(projects),
print_newline=not(opt.quiet),
always_print_percentage=opt.quiet)
objdir_project_map = dict()
@ -364,11 +380,10 @@ later is required to fix a server side protocol bug.
threads = set()
sem = _threading.Semaphore(self.jobs)
err_event = _threading.Event()
for project_list in objdir_project_map.values():
# Check for any errors before running any more tasks.
# ...we'll let existing threads finish, though.
if err_event.isSet() and not opt.force_broken:
if err_event.isSet() and opt.fail_fast:
break
sem.acquire()
@ -378,7 +393,8 @@ later is required to fix a server side protocol bug.
lock=lock,
fetched=fetched,
pm=pm,
err_event=err_event)
err_event=err_event,
clone_filter=self.manifest.CloneFilter)
if self.jobs > 1:
t = _threading.Thread(target = self._FetchProjectList,
kwargs = kwargs)
@ -392,25 +408,172 @@ later is required to fix a server side protocol bug.
for t in threads:
t.join()
# If we saw an error, exit with code 1 so that other scripts can check.
if err_event.isSet() and not opt.force_broken:
print('\nerror: Exited sync due to fetch errors', file=sys.stderr)
sys.exit(1)
pm.end()
self._fetch_times.Save()
if not self.manifest.IsArchive:
self._GCProjects(projects)
self._GCProjects(projects, opt, err_event)
return fetched
def _GCProjects(self, projects):
def _CheckoutWorker(self, opt, sem, project, *args, **kwargs):
"""Main function of the fetch threads.
Delegates most of the work to _CheckoutOne.
Args:
opt: Program options returned from optparse. See _Options().
projects: Projects to fetch.
sem: We'll release() this semaphore when we exit so that another thread
can be started up.
*args, **kwargs: Remaining arguments to pass to _CheckoutOne. See the
_CheckoutOne docstring for details.
"""
try:
return self._CheckoutOne(opt, project, *args, **kwargs)
finally:
sem.release()
def _CheckoutOne(self, opt, project, lock, pm, err_event, err_results):
"""Checkout work tree for one project
Args:
opt: Program options returned from optparse. See _Options().
project: Project object for the project to checkout.
lock: Lock for accessing objects that are shared amongst multiple
_CheckoutWorker() threads.
pm: Instance of a Project object. We will call pm.update() (with our
lock held).
err_event: We'll set this event in the case of an error (after printing
out info about the error).
err_results: A list of strings, paths to git repos where checkout
failed.
Returns:
Whether the fetch was successful.
"""
# We'll set to true once we've locked the lock.
did_lock = False
# Encapsulate everything in a try/except/finally so that:
# - We always set err_event in the case of an exception.
# - We always make sure we unlock the lock if we locked it.
start = time.time()
syncbuf = SyncBuffer(self.manifest.manifestProject.config,
detach_head=opt.detach_head)
success = False
try:
try:
project.Sync_LocalHalf(syncbuf, force_sync=opt.force_sync)
# Lock around all the rest of the code, since printing, updating a set
# and Progress.update() are not thread safe.
lock.acquire()
success = syncbuf.Finish()
did_lock = True
if not success:
err_event.set()
print('error: Cannot checkout %s' % (project.name),
file=sys.stderr)
raise _CheckoutError()
pm.update(msg=project.name)
except _CheckoutError:
pass
except Exception as e:
print('error: Cannot checkout %s: %s: %s' %
(project.name, type(e).__name__, str(e)),
file=sys.stderr)
err_event.set()
raise
finally:
if did_lock:
if not success:
err_results.append(project.relpath)
lock.release()
finish = time.time()
self.event_log.AddSync(project, event_log.TASK_SYNC_LOCAL,
start, finish, success)
return success
def _Checkout(self, all_projects, opt, err_event, err_results):
"""Checkout projects listed in all_projects
Args:
all_projects: List of all projects that should be checked out.
opt: Program options returned from optparse. See _Options().
err_event: We'll set this event in the case of an error (after printing
out info about the error).
err_results: A list of strings, paths to git repos where checkout
failed.
"""
# Perform checkouts in multiple threads when we are using partial clone.
# Without partial clone, all needed git objects are already downloaded,
# in this situation it's better to use only one process because the checkout
# would be mostly disk I/O; with partial clone, the objects are only
# downloaded when demanded (at checkout time), which is similar to the
# Sync_NetworkHalf case and parallelism would be helpful.
if self.manifest.CloneFilter:
syncjobs = self.jobs
else:
syncjobs = 1
lock = _threading.Lock()
pm = Progress('Checking out projects', len(all_projects))
threads = set()
sem = _threading.Semaphore(syncjobs)
for project in all_projects:
# Check for any errors before running any more tasks.
# ...we'll let existing threads finish, though.
if err_event.isSet() and opt.fail_fast:
break
sem.acquire()
if project.worktree:
kwargs = dict(opt=opt,
sem=sem,
project=project,
lock=lock,
pm=pm,
err_event=err_event,
err_results=err_results)
if syncjobs > 1:
t = _threading.Thread(target=self._CheckoutWorker,
kwargs=kwargs)
# Ensure that Ctrl-C will not freeze the repo process.
t.daemon = True
threads.add(t)
t.start()
else:
self._CheckoutWorker(**kwargs)
for t in threads:
t.join()
pm.end()
def _GCProjects(self, projects, opt, err_event):
gc_gitdirs = {}
for project in projects:
# Make sure pruning never kicks in with shared projects.
if len(project.manifest.GetProjectsWithName(project.name)) > 1:
print('Shared project %s found, disabling pruning.' % project.name)
project.bare_git.config('--replace-all', 'gc.pruneExpire', 'never')
print('%s: Shared project %s found, disabling pruning.' %
(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
has_dash_c = git_require((1, 7, 2))
@ -425,11 +588,10 @@ later is required to fix a server side protocol bug.
bare_git.gc('--auto')
return
config = {'pack.threads': cpu_count / jobs if cpu_count > jobs else 1}
config = {'pack.threads': cpu_count // jobs if cpu_count > jobs else 1}
threads = set()
sem = _threading.Semaphore(jobs)
err_event = _threading.Event()
def GC(bare_git):
try:
@ -444,7 +606,7 @@ later is required to fix a server side protocol bug.
sem.release()
for bare_git in gc_gitdirs.values():
if err_event.isSet():
if err_event.isSet() and opt.fail_fast:
break
sem.acquire()
t = _threading.Thread(target=GC, args=(bare_git,))
@ -455,10 +617,6 @@ later is required to fix a server side protocol bug.
for t in threads:
t.join()
if err_event.isSet():
print('\nerror: Exited sync due to gc errors', file=sys.stderr)
sys.exit(1)
def _ReloadManifest(self, manifest_name=None):
if manifest_name:
# Override calls _Unload already
@ -478,7 +636,7 @@ later is required to fix a server side protocol bug.
print('Failed to remove %s (%s)' % (os.path.join(path, '.git'), str(e)), file=sys.stderr)
print('error: Failed to delete obsolete path %s' % path, file=sys.stderr)
print(' remove manually, then run sync again', file=sys.stderr)
return -1
return 1
# Delete everything under the worktree, except for directories that contain
# another git project
@ -512,7 +670,7 @@ later is required to fix a server side protocol bug.
if failed:
print('error: Failed to delete obsolete path %s' % path, file=sys.stderr)
print(' remove manually, then run sync again', file=sys.stderr)
return -1
return 1
# Try deleting parent dirs if they are empty
project_dir = path
@ -525,7 +683,7 @@ later is required to fix a server side protocol bug.
return 0
def UpdateProjectList(self):
def UpdateProjectList(self, opt):
new_project_paths = []
for project in self.GetProjects(None, missing_ok=True):
if project.relpath:
@ -535,12 +693,10 @@ later is required to fix a server side protocol bug.
old_project_paths = []
if os.path.exists(file_path):
fd = open(file_path, 'r')
try:
with open(file_path, 'r') as fd:
old_project_paths = fd.read().split('\n')
finally:
fd.close()
for path in old_project_paths:
# In reversed order, so subfolders are deleted before parent folder.
for path in sorted(old_project_paths, reverse=True):
if not path:
continue
if path not in new_project_paths:
@ -559,178 +715,129 @@ later is required to fix a server side protocol bug.
revisionId = None,
groups = None)
if project.IsDirty():
if project.IsDirty() and opt.force_remove_dirty:
print('WARNING: Removing dirty project "%s": uncommitted changes '
'erased' % project.relpath, file=sys.stderr)
self._DeleteProject(project.worktree)
elif project.IsDirty():
print('error: Cannot remove project "%s": uncommitted changes '
'are present' % project.relpath, file=sys.stderr)
print(' commit changes, then run sync again',
file=sys.stderr)
return -1
return 1
elif self._DeleteProject(project.worktree):
return -1
return 1
new_project_paths.sort()
fd = open(file_path, 'w')
try:
with open(file_path, 'w') as fd:
fd.write('\n'.join(new_project_paths))
fd.write('\n')
finally:
fd.close()
return 0
def Execute(self, opt, args):
if opt.jobs:
self.jobs = opt.jobs
if self.jobs > 1:
soft_limit, _ = _rlimit_nofile()
self.jobs = min(self.jobs, (soft_limit - 5) / 3)
if opt.network_only and opt.detach_head:
print('error: cannot combine -n and -d', file=sys.stderr)
def _SmartSyncSetup(self, opt, smart_sync_manifest_path):
if not self.manifest.manifest_server:
print('error: cannot smart sync: no manifest server defined in '
'manifest', file=sys.stderr)
sys.exit(1)
if opt.network_only and opt.local_only:
print('error: cannot combine -n and -l', file=sys.stderr)
sys.exit(1)
if opt.manifest_name and opt.smart_sync:
print('error: cannot combine -m and -s', file=sys.stderr)
sys.exit(1)
if opt.manifest_name and opt.smart_tag:
print('error: cannot combine -m and -t', file=sys.stderr)
sys.exit(1)
if opt.manifest_server_username or opt.manifest_server_password:
if not (opt.smart_sync or opt.smart_tag):
print('error: -u and -p may only be combined with -s or -t',
file=sys.stderr)
sys.exit(1)
if None in [opt.manifest_server_username, opt.manifest_server_password]:
print('error: both -u and -p must be given', file=sys.stderr)
sys.exit(1)
if opt.manifest_name:
self.manifest.Override(opt.manifest_name)
manifest_server = self.manifest.manifest_server
if not opt.quiet:
print('Using manifest server %s' % manifest_server)
manifest_name = opt.manifest_name
smart_sync_manifest_name = "smart_sync_override.xml"
smart_sync_manifest_path = os.path.join(
self.manifest.manifestProject.worktree, smart_sync_manifest_name)
if opt.smart_sync or opt.smart_tag:
if not self.manifest.manifest_server:
print('error: cannot smart sync: no manifest server defined in '
'manifest', file=sys.stderr)
sys.exit(1)
manifest_server = self.manifest.manifest_server
if not opt.quiet:
print('Using manifest server %s' % manifest_server)
if not '@' in manifest_server:
username = None
password = None
if opt.manifest_server_username and opt.manifest_server_password:
username = opt.manifest_server_username
password = opt.manifest_server_password
else:
try:
info = netrc.netrc()
except IOError:
# .netrc file does not exist or could not be opened
pass
else:
try:
parse_result = urllib.parse.urlparse(manifest_server)
if parse_result.hostname:
auth = info.authenticators(parse_result.hostname)
if auth:
username, _account, password = auth
else:
print('No credentials found for %s in .netrc'
% parse_result.hostname, file=sys.stderr)
except netrc.NetrcParseError as e:
print('Error parsing .netrc file: %s' % e, file=sys.stderr)
if (username and password):
manifest_server = manifest_server.replace('://', '://%s:%s@' %
(username, password),
1)
transport = PersistentTransport(manifest_server)
if manifest_server.startswith('persistent-'):
manifest_server = manifest_server[len('persistent-'):]
try:
server = xmlrpc.client.Server(manifest_server, transport=transport)
if opt.smart_sync:
p = self.manifest.manifestProject
b = p.GetBranch(p.CurrentBranch)
branch = b.merge
if branch.startswith(R_HEADS):
branch = branch[len(R_HEADS):]
env = os.environ.copy()
if 'SYNC_TARGET' in env:
target = env['SYNC_TARGET']
[success, manifest_str] = server.GetApprovedManifest(branch, target)
elif 'TARGET_PRODUCT' in env and 'TARGET_BUILD_VARIANT' in env:
target = '%s-%s' % (env['TARGET_PRODUCT'],
env['TARGET_BUILD_VARIANT'])
[success, manifest_str] = server.GetApprovedManifest(branch, target)
else:
[success, manifest_str] = server.GetApprovedManifest(branch)
else:
assert(opt.smart_tag)
[success, manifest_str] = server.GetManifest(opt.smart_tag)
if success:
manifest_name = smart_sync_manifest_name
try:
f = open(smart_sync_manifest_path, 'w')
try:
f.write(manifest_str)
finally:
f.close()
except IOError as e:
print('error: cannot write manifest to %s:\n%s'
% (smart_sync_manifest_path, e),
file=sys.stderr)
sys.exit(1)
self._ReloadManifest(manifest_name)
else:
print('error: manifest server RPC call failed: %s' %
manifest_str, file=sys.stderr)
sys.exit(1)
except (socket.error, IOError, xmlrpc.client.Fault) as e:
print('error: cannot connect to manifest server %s:\n%s'
% (self.manifest.manifest_server, e), file=sys.stderr)
sys.exit(1)
except xmlrpc.client.ProtocolError as e:
print('error: cannot connect to manifest server %s:\n%d %s'
% (self.manifest.manifest_server, e.errcode, e.errmsg),
file=sys.stderr)
sys.exit(1)
else: # Not smart sync or smart tag mode
if os.path.isfile(smart_sync_manifest_path):
if not '@' in manifest_server:
username = None
password = None
if opt.manifest_server_username and opt.manifest_server_password:
username = opt.manifest_server_username
password = opt.manifest_server_password
else:
try:
platform_utils.remove(smart_sync_manifest_path)
except OSError as e:
print('error: failed to remove existing smart sync override manifest: %s' %
e, file=sys.stderr)
info = netrc.netrc()
except IOError:
# .netrc file does not exist or could not be opened
pass
else:
try:
parse_result = urllib.parse.urlparse(manifest_server)
if parse_result.hostname:
auth = info.authenticators(parse_result.hostname)
if auth:
username, _account, password = auth
else:
print('No credentials found for %s in .netrc'
% parse_result.hostname, file=sys.stderr)
except netrc.NetrcParseError as e:
print('Error parsing .netrc file: %s' % e, file=sys.stderr)
rp = self.manifest.repoProject
rp.PreSync()
if (username and password):
manifest_server = manifest_server.replace('://', '://%s:%s@' %
(username, password),
1)
mp = self.manifest.manifestProject
mp.PreSync()
transport = PersistentTransport(manifest_server)
if manifest_server.startswith('persistent-'):
manifest_server = manifest_server[len('persistent-'):]
if opt.repo_upgraded:
_PostRepoUpgrade(self.manifest, quiet=opt.quiet)
try:
server = xmlrpc.client.Server(manifest_server, transport=transport)
if opt.smart_sync:
p = self.manifest.manifestProject
b = p.GetBranch(p.CurrentBranch)
branch = b.merge
if branch.startswith(R_HEADS):
branch = branch[len(R_HEADS):]
env = os.environ.copy()
if 'SYNC_TARGET' in env:
target = env['SYNC_TARGET']
[success, manifest_str] = server.GetApprovedManifest(branch, target)
elif 'TARGET_PRODUCT' in env and 'TARGET_BUILD_VARIANT' in env:
target = '%s-%s' % (env['TARGET_PRODUCT'],
env['TARGET_BUILD_VARIANT'])
[success, manifest_str] = server.GetApprovedManifest(branch, target)
else:
[success, manifest_str] = server.GetApprovedManifest(branch)
else:
assert(opt.smart_tag)
[success, manifest_str] = server.GetManifest(opt.smart_tag)
if success:
manifest_name = os.path.basename(smart_sync_manifest_path)
try:
with open(smart_sync_manifest_path, 'w') as f:
f.write(manifest_str)
except IOError as e:
print('error: cannot write manifest to %s:\n%s'
% (smart_sync_manifest_path, e),
file=sys.stderr)
sys.exit(1)
self._ReloadManifest(manifest_name)
else:
print('error: manifest server RPC call failed: %s' %
manifest_str, file=sys.stderr)
sys.exit(1)
except (socket.error, IOError, xmlrpc.client.Fault) as e:
print('error: cannot connect to manifest server %s:\n%s'
% (self.manifest.manifest_server, e), file=sys.stderr)
sys.exit(1)
except xmlrpc.client.ProtocolError as e:
print('error: cannot connect to manifest server %s:\n%d %s'
% (self.manifest.manifest_server, e.errcode, e.errmsg),
file=sys.stderr)
sys.exit(1)
return manifest_name
def _UpdateManifestProject(self, opt, mp, manifest_name):
"""Fetch & update the local manifest project."""
if not opt.local_only:
start = time.time()
success = mp.Sync_NetworkHalf(quiet=opt.quiet,
current_branch_only=opt.current_branch_only,
no_tags=opt.no_tags,
optimized_fetch=opt.optimized_fetch,
submodules=self.manifest.HasSubmodules)
submodules=self.manifest.HasSubmodules,
clone_filter=self.manifest.CloneFilter)
finish = time.time()
self.event_log.AddSync(mp, event_log.TASK_SYNC_NETWORK,
start, finish, success)
@ -744,10 +851,68 @@ later is required to fix a server side protocol bug.
start, time.time(), clean)
if not clean:
sys.exit(1)
self._ReloadManifest(manifest_name)
self._ReloadManifest(opt.manifest_name)
if opt.jobs is None:
self.jobs = self.manifest.default.sync_j
def ValidateOptions(self, opt, args):
if opt.force_broken:
print('warning: -f/--force-broken is now the default behavior, and the '
'options are deprecated', file=sys.stderr)
if opt.network_only and opt.detach_head:
self.OptionParser.error('cannot combine -n and -d')
if opt.network_only and opt.local_only:
self.OptionParser.error('cannot combine -n and -l')
if opt.manifest_name and opt.smart_sync:
self.OptionParser.error('cannot combine -m and -s')
if opt.manifest_name and opt.smart_tag:
self.OptionParser.error('cannot combine -m and -t')
if opt.manifest_server_username or opt.manifest_server_password:
if not (opt.smart_sync or opt.smart_tag):
self.OptionParser.error('-u and -p may only be combined with -s or -t')
if None in [opt.manifest_server_username, opt.manifest_server_password]:
self.OptionParser.error('both -u and -p must be given')
def Execute(self, opt, args):
if opt.jobs:
self.jobs = opt.jobs
if self.jobs > 1:
soft_limit, _ = _rlimit_nofile()
self.jobs = min(self.jobs, (soft_limit - 5) // 3)
if opt.manifest_name:
self.manifest.Override(opt.manifest_name)
manifest_name = opt.manifest_name
smart_sync_manifest_path = os.path.join(
self.manifest.manifestProject.worktree, 'smart_sync_override.xml')
if opt.smart_sync or opt.smart_tag:
manifest_name = self._SmartSyncSetup(opt, smart_sync_manifest_path)
else:
if os.path.isfile(smart_sync_manifest_path):
try:
platform_utils.remove(smart_sync_manifest_path)
except OSError as e:
print('error: failed to remove existing smart sync override manifest: %s' %
e, file=sys.stderr)
err_event = _threading.Event()
rp = self.manifest.repoProject
rp.PreSync()
mp = self.manifest.manifestProject
mp.PreSync()
if opt.repo_upgraded:
_PostRepoUpgrade(self.manifest, quiet=opt.quiet)
if not opt.mp_update:
print('Skipping update of local manifest project.')
else:
self._UpdateManifestProject(opt, mp, manifest_name)
if self.gitc_manifest:
gitc_manifest_projects = self.GetProjects(args,
missing_ok=True)
@ -787,6 +952,10 @@ later is required to fix a server side protocol bug.
missing_ok=True,
submodules_ok=opt.fetch_submodules)
err_network_sync = False
err_update_projects = False
err_checkout = False
self._fetch_times = _FetchTimes(self.manifest)
if not opt.local_only:
to_fetch = []
@ -796,10 +965,14 @@ later is required to fix a server side protocol bug.
to_fetch.extend(all_projects)
to_fetch.sort(key=self._fetch_times.Get, reverse=True)
fetched = self._Fetch(to_fetch, opt)
fetched = self._Fetch(to_fetch, opt, err_event)
_PostRepoFetch(rp, opt.no_repo_verify)
if opt.network_only:
# bail out now; the rest touches the working tree
if err_event.isSet():
print('\nerror: Exited sync due to fetch errors.\n', file=sys.stderr)
sys.exit(1)
return
# Iteratively fetch missing and/or nested unregistered submodules
@ -821,35 +994,56 @@ later is required to fix a server side protocol bug.
if previously_missing_set == missing_set:
break
previously_missing_set = missing_set
fetched.update(self._Fetch(missing, opt))
fetched.update(self._Fetch(missing, opt, err_event))
# If we saw an error, exit with code 1 so that other scripts can check.
if err_event.isSet():
err_network_sync = True
if opt.fail_fast:
print('\nerror: Exited sync due to fetch errors.\n'
'Local checkouts *not* updated. Resolve network issues & '
'retry.\n'
'`repo sync -l` will update some local checkouts.',
file=sys.stderr)
sys.exit(1)
if self.manifest.IsMirror or self.manifest.IsArchive:
# bail out now, we have no working tree
return
if self.UpdateProjectList():
sys.exit(1)
if self.UpdateProjectList(opt):
err_event.set()
err_update_projects = True
if opt.fail_fast:
print('\nerror: Local checkouts *not* updated.', file=sys.stderr)
sys.exit(1)
syncbuf = SyncBuffer(mp.config,
detach_head = opt.detach_head)
pm = Progress('Syncing work tree', len(all_projects))
for project in all_projects:
pm.update()
if project.worktree:
start = time.time()
project.Sync_LocalHalf(syncbuf, force_sync=opt.force_sync)
self.event_log.AddSync(project, event_log.TASK_SYNC_LOCAL,
start, time.time(), syncbuf.Recently())
pm.end()
print(file=sys.stderr)
if not syncbuf.Finish():
sys.exit(1)
err_results = []
self._Checkout(all_projects, opt, err_event, err_results)
if err_event.isSet():
err_checkout = True
# NB: We don't exit here because this is the last step.
# If there's a notice that's supposed to print at the end of the sync, print
# it now...
if self.manifest.notice:
print(self.manifest.notice)
# If we saw an error, exit with code 1 so that other scripts can check.
if err_event.isSet():
print('\nerror: Unable to fully sync the tree.', file=sys.stderr)
if err_network_sync:
print('error: Downloading network changes failed.', file=sys.stderr)
if err_update_projects:
print('error: Updating local project lists failed.', file=sys.stderr)
if err_checkout:
print('error: Checking out local projects failed.', file=sys.stderr)
if err_results:
print('Failing repos:\n%s' % '\n'.join(err_results), file=sys.stderr)
print('Try re-running with "-j1 --fail-fast" to exit at the first error.',
file=sys.stderr)
sys.exit(1)
def _PostRepoUpgrade(manifest, quiet=False):
wrapper = Wrapper()
if wrapper.NeedSetupGnuPG():
@ -947,11 +1141,8 @@ class _FetchTimes(object):
def _Load(self):
if self._times is None:
try:
f = open(self._path)
try:
with open(self._path) as f:
self._times = json.load(f)
finally:
f.close()
except (IOError, ValueError):
try:
platform_utils.remove(self._path)
@ -971,11 +1162,8 @@ class _FetchTimes(object):
del self._times[name]
try:
f = open(self._path, 'w')
try:
with open(self._path, 'w') as f:
json.dump(self._times, f, indent=2)
finally:
f.close()
except (IOError, TypeError):
try:
platform_utils.remove(self._path)

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2008 The Android Open Source Project
#
@ -220,7 +221,9 @@ Gerrit Code Review: https://www.gerritcodereview.com/
for commit in commit_list:
print(' %s' % commit)
sys.stdout.write('to %s (y/N)? ' % remote.review)
print('to %s (y/N)? ' % remote.review, end='')
# TODO: When we require Python 3, use flush=True w/print above.
sys.stdout.flush()
answer = sys.stdin.readline().strip().lower()
answer = answer in ('y', 'yes', '1', 'true', 't')
@ -268,11 +271,6 @@ Gerrit Code Review: https://www.gerritcodereview.com/
branches[project.name] = b
script.append('')
script = [ x.encode('utf-8')
if issubclass(type(x), unicode)
else x
for x in script ]
script = Editor.EditString("\n".join(script)).split("\n")
project_re = re.compile(r'^#?\s*project\s*([^\s]+)/:$')
@ -359,10 +357,13 @@ Gerrit Code Review: https://www.gerritcodereview.com/
# if they want to auto upload, let's not ask because it could be automated
if answer is None:
sys.stdout.write('Uncommitted changes in ' + branch.project.name)
sys.stdout.write(' (did you forget to amend?):\n')
sys.stdout.write('\n'.join(changes) + '\n')
sys.stdout.write('Continue uploading? (y/N) ')
print()
print('Uncommitted changes in %s (did you forget to amend?):'
% branch.project.name)
print('\n'.join(changes))
print('Continue uploading? (y/N) ', end='')
# TODO: When we require Python 3, use flush=True w/print above.
sys.stdout.flush()
a = sys.stdin.readline().strip().lower()
if a not in ('y', 'yes', 't', 'true', 'on'):
print("skipping upload", file=sys.stderr)

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2009 The Android Open Source Project
#
@ -16,7 +17,7 @@
from __future__ import print_function
import sys
from command import Command, MirrorSafeCommand
from git_command import git
from git_command import git, RepoSourceVersion, user_agent
from git_refs import HEAD
class Version(Command, MirrorSafeCommand):
@ -33,12 +34,20 @@ class Version(Command, MirrorSafeCommand):
rp = self.manifest.repoProject
rem = rp.GetRemote(rp.remote.name)
print('repo version %s' % rp.work_git.describe(HEAD))
# These might not be the same. Report them both.
src_ver = RepoSourceVersion()
rp_ver = rp.bare_git.describe(HEAD)
print('repo version %s' % rp_ver)
print(' (from %s)' % rem.url)
if Version.wrapper_path is not None:
print('repo launcher version %s' % Version.wrapper_version)
print(' (from %s)' % Version.wrapper_path)
print(git.version().strip())
if src_ver != rp_ver:
print(' (currently at %s)' % src_ver)
print('repo User-Agent %s' % user_agent.repo)
print('git %s' % git.version_tuple().full)
print('git User-Agent %s' % user_agent.git)
print('Python %s' % sys.version)

2
tests/fixtures/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/.repo_not.present.gitconfig.json
/.repo_test.gitconfig.json

60
tests/test_editor.py Normal file
View File

@ -0,0 +1,60 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2019 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unittests for the editor.py module."""
from __future__ import print_function
import unittest
from editor import Editor
class EditorTestCase(unittest.TestCase):
"""Take care of resetting Editor state across tests."""
def setUp(self):
self.setEditor(None)
def tearDown(self):
self.setEditor(None)
@staticmethod
def setEditor(editor):
Editor._editor = editor
class GetEditor(EditorTestCase):
"""Check GetEditor behavior."""
def test_basic(self):
"""Basic checking of _GetEditor."""
self.setEditor(':')
self.assertEqual(':', Editor._GetEditor())
class EditString(EditorTestCase):
"""Check EditString behavior."""
def test_no_editor(self):
"""Check behavior when no editor is available."""
self.setEditor(':')
self.assertEqual('foo', Editor.EditString('foo'))
def test_cat_editor(self):
"""Check behavior when editor is `cat`."""
self.setEditor('cat')
self.assertEqual('foo', Editor.EditString('foo'))

78
tests/test_git_command.py Normal file
View File

@ -0,0 +1,78 @@
# -*- coding:utf-8 -*-
#
# Copyright 2019 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unittests for the git_command.py module."""
from __future__ import print_function
import re
import unittest
import git_command
class GitCallUnitTest(unittest.TestCase):
"""Tests the _GitCall class (via git_command.git)."""
def test_version_tuple(self):
"""Check git.version_tuple() handling."""
ver = git_command.git.version_tuple()
self.assertIsNotNone(ver)
# We don't dive too deep into the values here to avoid having to update
# whenever git versions change. We do check relative to this min version
# as this is what `repo` itself requires via MIN_GIT_VERSION.
MIN_GIT_VERSION = (2, 10, 2)
self.assertTrue(isinstance(ver.major, int))
self.assertTrue(isinstance(ver.minor, int))
self.assertTrue(isinstance(ver.micro, int))
self.assertGreater(ver.major, MIN_GIT_VERSION[0] - 1)
self.assertGreaterEqual(ver.micro, 0)
self.assertGreaterEqual(ver.major, 0)
self.assertGreaterEqual(ver, MIN_GIT_VERSION)
self.assertLess(ver, (9999, 9999, 9999))
self.assertNotEqual('', ver.full)
class UserAgentUnitTest(unittest.TestCase):
"""Tests the UserAgent function."""
def test_smoke_os(self):
"""Make sure UA OS setting returns something useful."""
os_name = git_command.user_agent.os
# We can't dive too deep because of OS/tool differences, but we can check
# the general form.
m = re.match(r'^[^ ]+$', os_name)
self.assertIsNotNone(m)
def test_smoke_repo(self):
"""Make sure repo UA returns something useful."""
ua = git_command.user_agent.repo
# We can't dive too deep because of OS/tool differences, but we can check
# the general form.
m = re.match(r'^git-repo/[^ ]+ ([^ ]+) git/[^ ]+ Python/[0-9.]+', ua)
self.assertIsNotNone(m)
def test_smoke_git(self):
"""Make sure git UA returns something useful."""
ua = git_command.user_agent.git
# We can't dive too deep because of OS/tool differences, but we can check
# the general form.
m = re.match(r'^git/[^ ]+ ([^ ]+) git-repo/[^ ]+', ua)
self.assertIsNotNone(m)

View File

@ -1,3 +1,23 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2009 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unittests for the git_config.py module."""
from __future__ import print_function
import os
import unittest

View File

@ -0,0 +1,85 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2019 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unittests for the manifest_xml.py module."""
from __future__ import print_function
import unittest
import error
import manifest_xml
class ManifestValidateFilePaths(unittest.TestCase):
"""Check _ValidateFilePaths helper.
This doesn't access a real filesystem.
"""
def check_both(self, *args):
manifest_xml.XmlManifest._ValidateFilePaths('copyfile', *args)
manifest_xml.XmlManifest._ValidateFilePaths('linkfile', *args)
def test_normal_path(self):
"""Make sure good paths are accepted."""
self.check_both('foo', 'bar')
self.check_both('foo/bar', 'bar')
self.check_both('foo', 'bar/bar')
self.check_both('foo/bar', 'bar/bar')
def test_symlink_targets(self):
"""Some extra checks for symlinks."""
def check(*args):
manifest_xml.XmlManifest._ValidateFilePaths('linkfile', *args)
# We allow symlinks to end in a slash since we allow them to point to dirs
# in general. Technically the slash isn't necessary.
check('foo/', 'bar')
# We allow a single '.' to get a reference to the project itself.
check('.', 'bar')
def test_bad_paths(self):
"""Make sure bad paths (src & dest) are rejected."""
PATHS = (
'..',
'../',
'./',
'foo/',
'./foo',
'../foo',
'foo/./bar',
'foo/../../bar',
'/foo',
'./../foo',
'.git/foo',
# Check case folding.
'.GIT/foo',
'blah/.git/foo',
'.repo/foo',
'.repoconfig',
# Block ~ due to 8.3 filenames on Windows filesystems.
'~',
'foo~',
'blah/foo~',
# Block Unicode characters that get normalized out by filesystems.
u'foo\u200Cbar',
)
for path in PATHS:
self.assertRaises(
error.ManifestInvalidPathError, self.check_both, path, 'a')
self.assertRaises(
error.ManifestInvalidPathError, self.check_both, 'a', path)

363
tests/test_project.py Normal file
View File

@ -0,0 +1,363 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2019 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unittests for the project.py module."""
from __future__ import print_function
import contextlib
import os
import shutil
import subprocess
import tempfile
import unittest
import error
import git_config
import project
@contextlib.contextmanager
def TempGitTree():
"""Create a new empty git checkout for testing."""
# TODO(vapier): Convert this to tempfile.TemporaryDirectory once we drop
# Python 2 support entirely.
try:
tempdir = tempfile.mkdtemp(prefix='repo-tests')
subprocess.check_call(['git', 'init'], cwd=tempdir)
yield tempdir
finally:
shutil.rmtree(tempdir)
class RepoHookShebang(unittest.TestCase):
"""Check shebang parsing in RepoHook."""
def test_no_shebang(self):
"""Lines w/out shebangs should be rejected."""
DATA = (
'',
'# -*- coding:utf-8 -*-\n',
'#\n# foo\n',
'# Bad shebang in script\n#!/foo\n'
)
for data in DATA:
self.assertIsNone(project.RepoHook._ExtractInterpFromShebang(data))
def test_direct_interp(self):
"""Lines whose shebang points directly to the interpreter."""
DATA = (
('#!/foo', '/foo'),
('#! /foo', '/foo'),
('#!/bin/foo ', '/bin/foo'),
('#! /usr/foo ', '/usr/foo'),
('#! /usr/foo -args', '/usr/foo'),
)
for shebang, interp in DATA:
self.assertEqual(project.RepoHook._ExtractInterpFromShebang(shebang),
interp)
def test_env_interp(self):
"""Lines whose shebang launches through `env`."""
DATA = (
('#!/usr/bin/env foo', 'foo'),
('#!/bin/env foo', 'foo'),
('#! /bin/env /bin/foo ', '/bin/foo'),
)
for shebang, interp in DATA:
self.assertEqual(project.RepoHook._ExtractInterpFromShebang(shebang),
interp)
class FakeProject(object):
"""A fake for Project for basic functionality."""
def __init__(self, worktree):
self.worktree = worktree
self.gitdir = os.path.join(worktree, '.git')
self.name = 'fakeproject'
self.work_git = project.Project._GitGetByExec(
self, bare=False, gitdir=self.gitdir)
self.bare_git = project.Project._GitGetByExec(
self, bare=True, gitdir=self.gitdir)
self.config = git_config.GitConfig.ForRepository(gitdir=self.gitdir)
class ReviewableBranchTests(unittest.TestCase):
"""Check ReviewableBranch behavior."""
def test_smoke(self):
"""A quick run through everything."""
with TempGitTree() as tempdir:
fakeproj = FakeProject(tempdir)
# Generate some commits.
with open(os.path.join(tempdir, 'readme'), 'w') as fp:
fp.write('txt')
fakeproj.work_git.add('readme')
fakeproj.work_git.commit('-mAdd file')
fakeproj.work_git.checkout('-b', 'work')
fakeproj.work_git.rm('-f', 'readme')
fakeproj.work_git.commit('-mDel file')
# Start off with the normal details.
rb = project.ReviewableBranch(
fakeproj, fakeproj.config.GetBranch('work'), 'master')
self.assertEqual('work', rb.name)
self.assertEqual(1, len(rb.commits))
self.assertIn('Del file', rb.commits[0])
d = rb.unabbrev_commits
self.assertEqual(1, len(d))
short, long = next(iter(d.items()))
self.assertTrue(long.startswith(short))
self.assertTrue(rb.base_exists)
# Hard to assert anything useful about this.
self.assertTrue(rb.date)
# Now delete the tracking branch!
fakeproj.work_git.branch('-D', 'master')
rb = project.ReviewableBranch(
fakeproj, fakeproj.config.GetBranch('work'), 'master')
self.assertEqual(0, len(rb.commits))
self.assertFalse(rb.base_exists)
# Hard to assert anything useful about this.
self.assertTrue(rb.date)
class CopyLinkTestCase(unittest.TestCase):
"""TestCase for stub repo client checkouts.
It'll have a layout like:
tempdir/ # self.tempdir
checkout/ # self.topdir
git-project/ # self.worktree
Attributes:
tempdir: A dedicated temporary directory.
worktree: The top of the repo client checkout.
topdir: The top of a project checkout.
"""
def setUp(self):
self.tempdir = tempfile.mkdtemp(prefix='repo_tests')
self.topdir = os.path.join(self.tempdir, 'checkout')
self.worktree = os.path.join(self.topdir, 'git-project')
os.makedirs(self.topdir)
os.makedirs(self.worktree)
def tearDown(self):
shutil.rmtree(self.tempdir, ignore_errors=True)
@staticmethod
def touch(path):
with open(path, 'w') as f:
pass
def assertExists(self, path, msg=None):
"""Make sure |path| exists."""
if os.path.exists(path):
return
if msg is None:
msg = ['path is missing: %s' % path]
while path != '/':
path = os.path.dirname(path)
if not path:
# If we're given something like "foo", abort once we get to "".
break
result = os.path.exists(path)
msg.append('\tos.path.exists(%s): %s' % (path, result))
if result:
msg.append('\tcontents: %r' % os.listdir(path))
break
msg = '\n'.join(msg)
raise self.failureException(msg)
class CopyFile(CopyLinkTestCase):
"""Check _CopyFile handling."""
def CopyFile(self, src, dest):
return project._CopyFile(self.worktree, src, self.topdir, dest)
def test_basic(self):
"""Basic test of copying a file from a project to the toplevel."""
src = os.path.join(self.worktree, 'foo.txt')
self.touch(src)
cf = self.CopyFile('foo.txt', 'foo')
cf._Copy()
self.assertExists(os.path.join(self.topdir, 'foo'))
def test_src_subdir(self):
"""Copy a file from a subdir of a project."""
src = os.path.join(self.worktree, 'bar', 'foo.txt')
os.makedirs(os.path.dirname(src))
self.touch(src)
cf = self.CopyFile('bar/foo.txt', 'new.txt')
cf._Copy()
self.assertExists(os.path.join(self.topdir, 'new.txt'))
def test_dest_subdir(self):
"""Copy a file to a subdir of a checkout."""
src = os.path.join(self.worktree, 'foo.txt')
self.touch(src)
cf = self.CopyFile('foo.txt', 'sub/dir/new.txt')
self.assertFalse(os.path.exists(os.path.join(self.topdir, 'sub')))
cf._Copy()
self.assertExists(os.path.join(self.topdir, 'sub', 'dir', 'new.txt'))
def test_update(self):
"""Make sure changed files get copied again."""
src = os.path.join(self.worktree, 'foo.txt')
dest = os.path.join(self.topdir, 'bar')
with open(src, 'w') as f:
f.write('1st')
cf = self.CopyFile('foo.txt', 'bar')
cf._Copy()
self.assertExists(dest)
with open(dest) as f:
self.assertEqual(f.read(), '1st')
with open(src, 'w') as f:
f.write('2nd!')
cf._Copy()
with open(dest) as f:
self.assertEqual(f.read(), '2nd!')
def test_src_block_symlink(self):
"""Do not allow reading from a symlinked path."""
src = os.path.join(self.worktree, 'foo.txt')
sym = os.path.join(self.worktree, 'sym')
self.touch(src)
os.symlink('foo.txt', sym)
self.assertExists(sym)
cf = self.CopyFile('sym', 'foo')
self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
def test_src_block_symlink_traversal(self):
"""Do not allow reading through a symlink dir."""
src = os.path.join(self.worktree, 'bar', 'passwd')
os.symlink('/etc', os.path.join(self.worktree, 'bar'))
self.assertExists(src)
cf = self.CopyFile('bar/foo.txt', 'foo')
self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
def test_src_block_dir(self):
"""Do not allow copying from a directory."""
src = os.path.join(self.worktree, 'dir')
os.makedirs(src)
cf = self.CopyFile('dir', 'foo')
self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
def test_dest_block_symlink(self):
"""Do not allow writing to a symlink."""
src = os.path.join(self.worktree, 'foo.txt')
self.touch(src)
os.symlink('dest', os.path.join(self.topdir, 'sym'))
cf = self.CopyFile('foo.txt', 'sym')
self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
def test_dest_block_symlink_traversal(self):
"""Do not allow writing through a symlink dir."""
src = os.path.join(self.worktree, 'foo.txt')
self.touch(src)
os.symlink('/tmp', os.path.join(self.topdir, 'sym'))
cf = self.CopyFile('foo.txt', 'sym/foo.txt')
self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
def test_src_block_dir(self):
"""Do not allow copying to a directory."""
src = os.path.join(self.worktree, 'foo.txt')
self.touch(src)
os.makedirs(os.path.join(self.topdir, 'dir'))
cf = self.CopyFile('foo.txt', 'dir')
self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
class LinkFile(CopyLinkTestCase):
"""Check _LinkFile handling."""
def LinkFile(self, src, dest):
return project._LinkFile(self.worktree, src, self.topdir, dest)
def test_basic(self):
"""Basic test of linking a file from a project into the toplevel."""
src = os.path.join(self.worktree, 'foo.txt')
self.touch(src)
lf = self.LinkFile('foo.txt', 'foo')
lf._Link()
dest = os.path.join(self.topdir, 'foo')
self.assertExists(dest)
self.assertTrue(os.path.islink(dest))
self.assertEqual('git-project/foo.txt', os.readlink(dest))
def test_src_subdir(self):
"""Link to a file in a subdir of a project."""
src = os.path.join(self.worktree, 'bar', 'foo.txt')
os.makedirs(os.path.dirname(src))
self.touch(src)
lf = self.LinkFile('bar/foo.txt', 'foo')
lf._Link()
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):
"""Link a file to a subdir of a checkout."""
src = os.path.join(self.worktree, 'foo.txt')
self.touch(src)
lf = self.LinkFile('foo.txt', 'sub/dir/foo/bar')
self.assertFalse(os.path.exists(os.path.join(self.topdir, 'sub')))
lf._Link()
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):
"""Make sure changed targets get updated."""
dest = os.path.join(self.topdir, 'sym')
src = os.path.join(self.worktree, 'foo.txt')
self.touch(src)
lf = self.LinkFile('foo.txt', 'sym')
lf._Link()
self.assertEqual('git-project/foo.txt', os.readlink(dest))
# Point the symlink somewhere else.
os.unlink(dest)
os.symlink('/', dest)
lf._Link()
self.assertEqual('git-project/foo.txt', os.readlink(dest))

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2015 The Android Open Source Project
#
@ -13,6 +14,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unittests for the wrapper.py module."""
from __future__ import print_function
import os
import unittest

22
tox.ini Normal file
View File

@ -0,0 +1,22 @@
# Copyright 2019 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# https://tox.readthedocs.io/
[tox]
envlist = py27, py36, py37, py38
[testenv]
deps = pytest
commands = {toxinidir}/run_tests

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
#
# Copyright (C) 2014 The Android Open Source Project
#
@ -15,7 +15,12 @@
# limitations under the License.
from __future__ import print_function
import imp
try:
from importlib.machinery import SourceFileLoader
_loader = lambda *args: SourceFileLoader(*args).load_module()
except ImportError:
import imp
_loader = lambda *args: imp.load_source(*args)
import os
@ -26,5 +31,5 @@ _wrapper_module = None
def Wrapper():
global _wrapper_module
if not _wrapper_module:
_wrapper_module = imp.load_source('wrapper', WrapperPath())
_wrapper_module = _loader('wrapper', WrapperPath())
return _wrapper_module