Compare commits

...

238 Commits

Author SHA1 Message Date
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
6e53844f1e Allow clobbering of existing tags from remote.
Bug: 120778183
Change-Id: Id44e2b68abc410a3afd4e07a3c943b0936347e38
2018-12-10 11:33:16 -08:00
d26146de7f platform_utils: Fix exception handling in _walk_windows_impl
Change-Id: I6b79cbc4c1bbbe17ffe8361fe1544434beaa9059
2018-11-06 09:25:35 +09:00
bd8f658823 Add option for git-repo to support 'silent' uploads
When --ne/--no-emails is added to 'repo upload' command line, gerrit
server will not generate notification emails.

project.py:Project.UploadForReview method is modified to accept a
string recognizable by gerrit to indicate different sets of destination
email addressees, but the upload command line allows only one option -
disable sending emails completely.

Default repo upload behavior is not being changed.

TEST=tried in the Chrome OS repo, observed that patches uploaded with
     --ne or --no-emails indeed do not trigger any emails, while
     patches uploaded without these command line options still trigger
     email notifications.

Change-Id: I0301edec984907aedac277d883bd0e6d3099aedc
2018-11-05 14:01:05 -08:00
713c5872fb upload: Unify option passing in ssh and other transports
Pass options through the refspec for all transports, including ssh.
This means the behavior will be more consistent between the ssh and
https cases.

A downside is that this prevents passing special characters in
reviewer options.  That already didn't work over https, so it seems
okay.  It could be fixed by using push options instead.

Change-Id: Ia38d16e350cb8cb0de14463bfb3d9724e13bc4bf
2018-11-05 13:32:22 -08:00
36391bf5ca Merge "init: --dissociate option to copy objects borrowed with --reference" 2018-10-28 23:30:50 +00:00
bed8b62345 Add support for long paths
* Add more file i/o wrappers in platform_utils to allow using
  long paths (length > MAX_PATH) on Windows.

* Paths using the long path syntax ("\\?\" prefix) should never
  escape the platform_utils API surface area, so that this
  specific syntax is not visible to the rest of the repo code base.

* Forward many calls from os.xxx to platform_utils.xxx in various place
  to ensure long paths support, specifically when repo decides to delete
  obsolete directories.

* There are more places that need to be converted to support long paths,
  this commit is an initial effort to unblock a few common use cases.

* Also, fix remove function to handle directory symlinks

Change-Id: If82ccc408e516e96ff7260be25f8fd2fe3f9571a
2018-10-22 08:16:35 -07:00
09f0abb0ef init: --dissociate option to copy objects borrowed with --reference
"repo init --reference" has two purposes: to decrease bandwidth used
at clone time, and to decrease disk usage afterward, by using the
reference repositories as an alternate store of objects even after
the clone. The downside is that it makes the borrowing repositories
dependent on the reference repositories, so it is easy to end up
with missing objects by mistake after a cleanup operation like "git
gc".

To prevent that, v2.3.0-rc0~30^2 (clone: --dissociate option to mark
that reference is only temporary, 2014-10-14), "git clone" gained a
--dissociate option that makes --reference reuse objects from the
reference repository at clone time but copy them over instead of
using the reference as an alternate. This is more straightforward to
use than plain --reference, at the cost of higher disk usage.

Introduce a --dissociate to "repo init" that brings the same benefits
to repo. The option is simply passed on to "git clone".

Change-Id: Ib50a549eb71e0a2b3e234aea57537923962a80d4
2018-10-19 23:51:23 +05:00
b3133a3164 Merge "update markdown/help header format" 2018-10-10 05:54:59 +00:00
3b24e7b557 update homepage URIs
Change-Id: I482a72bf296978625b1e82ef580b0e0d4d57ff25
2018-10-10 01:35:58 -04:00
b8f7bb04d0 update markdown/help header format
Since gitiles recommends using # headers over ---/=== underlines,
change the manifest-format.md over and all our help texts.

Change-Id: I96391d41fba769e9f26870d497cf7cf01c8d8ab3
2018-10-10 01:28:43 -04:00
3891b7519d manifest-format: convert to markdown
The gitiles system doesn't render .txt files, so convert this to .md
for better display online.

Change-Id: Ie12e46daf008dd8c97ae2ffd21fb68bd948fe625
2018-10-05 19:32:51 -04:00
2b42d288c0 Windows: Add support for creating symlinks as an unprivileged user
See https://blogs.windows.com/buildingapps/2016/12/02/symlinks-windows-10/
for announcement of new flag.

This change follow the same pattern as what was done in "go":
https://github.com/golang/go/pull/24307/files#diff-b87bc12e4da2497308f9ef746086e4f0

Change-Id: If1e99fefdd3f787598e695731019c34b9bfcd1c2
2018-10-03 09:41:09 -07:00
e469a0c741 fix some sync error while using python3
Change-Id: I70925e48756c356d48359679d8ad1b9e33a68595
2018-07-24 22:20:08 +08:00
65b0ba5aa0 Remove unused pylint suppressions
pylint is not used since bb5b1a0. The pyflakes cleanup mentioned in that
commit has not been done, but given that this project is no longer being
actively developed I don't think it's worth spending time doing it.

Leaving the pylint suppressions causes confusion because it leads people
to think that we are still using pylint.

Change-Id: If7d9f280a0f408c780f15915ffdb80579ae21f69
2018-07-24 22:20:08 +08:00
a6515fb952 Merge "Flush stderr on Windows" 2018-07-13 15:48:36 +00:00
993dcacd17 Fix the initial existence check for "repo"
Commit 27226e742d introduced a warning if
"repo" is not part of the bootstrapped REPO_URL. However, that check was
done too early, directly after the call to _Clone. As the _Clone function
does not actually clone but it only initializes and fetches, the check
needs to be moved to after the call to _Checkout.

To reproduce, call

repo init --no-clone-bundle --repo-branch=master -u https://android.googlesource.com/platform/manifest

which will currently always show the (bogus) warning message. With this
fix, the warning will only be shown if "repo" indeed does not exist.

While at it, also slightly improve the code by using os.path.join().

Change-Id: Ied89e24231addabab6075005065748df1ffa74c4
2018-07-13 17:21:47 +02:00
a9399846fa Flush stderr on Windows
While on Linux stderr is unbuffered, it is buffered on Windows. Always
flush stderr on Windows to ensure any error messages appear in the right
order to ease diagnosing.

Change-Id: I37300e384ecd3a51a321a48818f0114d6f3357a0
2018-07-13 16:23:50 +02:00
b10f0e5b9a hooks/pre-auto-gc-battery: allow gc to run on non-laptops
Desktops and servers tend to have no power sensor, thus on_ac_power returns
255 ("unknown").  Thus, let's take any answer other than 1 ("battery") as
no contraindication to run gc.

If that tool returns "unknown", there's no point in querying other sources
as it already queried them, and is smarter than us (can handle multiple
adapters).

Reported by: Xin Li <delphij@google.com>
Signed-off-by: Adam Borowski <kilobyte@angband.pl>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
(cherry picked from git.git commit 781262c5e7ad4a7813c528803117ed0d2e8c5172)
Signed-off-by: Fredrik Roubert <roubert@google.com>
Signed-off-by: Jonathan Nieder <jrn@google.com>
Change-Id: I51fe2eb1eb879492a61e8e09c86ee34d049036c1
2018-07-11 13:45:58 -07:00
da40341a3e manifest: Support a default upstream value
It's convenient to set upstream for all projects in a manifest instead of
repeating the same value for each project.

Change-Id: I946b1de4efb01b351c332dfad108fa7d4f443cba
2018-05-09 14:58:18 -06:00
8d4b106642 Merge "docs: repo-hooks: fix cwd details" 2018-04-26 07:57:46 +00:00
ed429c9f6f docs: repo-hooks: fix cwd details
The hooks are run from the top of the manifest checkout, not from the
individual git repos.  It's up to individual hooks to chdir as needed.

Change-Id: I53325e0c3dcaa9c250b02b223e78d238d2cbd36d
2018-04-25 00:14:06 -04:00
0f2e45a3a6 Pass refs to ls-remote
This will fix the issue of parsing large output locally

Change-Id: I9a5cf1238147a02c92a3fca53eab9bd57f9d16b4
2018-03-24 13:00:08 +05:30
cf7c0834cf Download latest patch when no patch is specified
When someone does "repo download -c <project> <change>"
without specifying a patch number, by default patch 1 is
downloaded. An alternative is to look for the latest patch
and download the same when no explicit patch is given.
This commit does the same by identifying the latest patch
using "git ls-remote".

Change-Id: Ia5fa7364415f53a3d9436df4643e38f3c90ded58
2018-03-17 16:29:23 +05:30
4ea1f0cabd Merge changes I9c1ab65f,I7b2027ae
* changes:
  init: Remove string concat in no-op os.path.join
  Support relative paths in --reference
2018-03-16 02:06:10 +00:00
7d52585ec4 Add a way to override the revision of an <extend-project>
This change adds support for the 'revision' attribute in
<extend-project>. This avoids the need to perform a <remove-project>
followed by a <project> in local manifests.

Change-Id: Id2834fcfc1ae0d74b3347bed3618f250bf696b1f
2018-03-15 09:55:54 -07:00
1f365701b3 Merge "implement optional 'sync-tags' in the manifest file" 2018-02-26 06:50:53 +00:00
ce7e02601c Take care of a tilde on cookie file path
This handles cookie file path like "~/.gitcookies".

Change-Id: I87ba120a940fff38073d520f83b70654e6a239ba
2018-02-26 08:53:08 +09:00
a32c92c206 implement optional 'sync-tags' in the manifest file
Allow the 'default' and 'project' element in the manifest
file to apply "--no-tags" option equivalent.

Change-Id: I7e0f8c17a0e25cca744d45df049076d203c52ff5
Signed-off-by: YOUNG HO CHA <ganadist@gmail.com>
2018-02-14 16:57:41 +09:00
5f0e57d2ca init: Remove string concat in no-op os.path.join
This also fixes a line length warning.

Change-Id: I9c1ab65f83a35581dd657a707c7bc3c69db2b1dc
2018-01-22 11:00:24 -06:00
baa0009355 Support relative paths in --reference
Put the correctly-expanded relative paths in objects/info/alternates.
From gitrepository-layout(5), this path should be "relative to the
object database, not to the repository".

Change-Id: I7b2027ae23cf7d367b80f5a187603c4cbacdb2de
2018-01-22 10:57:29 -06:00
685320b000 event_log: Fix order of parameters to Add method call
Change-Id: I5add20eadfde39806ef4b2cc819da0ae0bfec2f5
2018-01-10 11:05:14 +09:00
02c0ee6ae6 Sync correctly when subproject url is a relative url to its parent url
Issue: when subproject url is a relative in .gitmodules
repo tool cannot handle this and cause:
"fatal: '***' does not appear to be a git repository
 fatal: Could not read from remote repository."
issue.

Signed-off-by: Shouheng Zhang <shouheng.zhang@intel.com>
Change-Id: I2a24c291ea0074ba13a740b32a11c0c25975e72b
2017-12-21 09:20:25 +08:00
1dc36600ef Merge "Update commit-msg hook to version from Gerrit 2.14.6" 2017-12-07 01:13:17 +00:00
cbe8aeb52b Update commit-msg hook to version from Gerrit 2.14.6
Change-Id: I14403fea4d017b97be5131e695803f121d404af2
2017-12-06 10:42:46 -08:00
305a2d029f Support --push-option in upload subcommand
Change-Id: I44836f8c66ded5a96cbf5431912e027e681f6529
2017-11-13 15:48:49 -08:00
84e7e16d35 document repo hooks mechanism
Change-Id: I9e25b92c846f887f515efcc706cf5a869645e0ec
2017-11-10 21:53:59 -05:00
f46902a800 forall: Clarify expansion of REPO_ environment values with -c
If a user executes:

  repo forall -c echo $REPO_PROJECT

then $REPO_NAME is expanded by the user's shell first, and passed
as $1 to the shell that executes echo. This will either result in
no output, or output of whatever REPO_NAME is set to in the user's
shell. Either way, this is an unexpected result.

The correct way to do it is:

  repo forall -c 'echo $REPO_PROJECT'

such that $REPO_NAME is passed in to the shell literally, and then
expanded to the value set in the environment that was passed to
the shell.

Update the documentation to make this clearer.

Change-Id: I713caee914172ad8d8f0fafacd27026502436f0d
2017-10-31 13:07:55 +09:00
c00d28b767 Set GIT_SSH_VARIANT when setting GIT_SSH
Make it explicit that the ssh wrapper we use for control master
support accepts OpenSSH-compatible command line arguments instead of
asking Git to guess.

The GIT_SSH_VARIANT setting was introduced in Git v2.13.0-rc0~3^2~2
(2017-02-01) as a more reliable detection method than relying on the
ssh command name.  Fortunately the default variant was 'ssh' (i.e.,
OpenSSH-compatible) so this wasn't initially required.

Now Git wants to start using more OpenSSH features
(-o SendEnv=GIT_PROTOCOL), and in order to do so its ssh variant
detection will need to be tweaked.  Set GIT_SSH_VARIANT explicitly
so this helper can continue to work regardless of how Git modifies
its autodetection.

Reported-by: William Yan <wyan@google.com>
Change-Id: I6bf2c53b4eb5303a429eae6cb68e0a5ccce89064
2017-10-19 14:39:26 -07:00
788e9626cc Provide more specific error message for symlinks errors on Windows
Change-Id: Ia6099beef37ae6b6143eba243fe7fbe02b74a9bb
2017-08-31 13:49:59 -07:00
cd892a38a6 Allow quotes in editor command on Windows
This change allows setting the EDITOR env. variable to point to a
program location that contains quotes and spaces.

For example:

> set EDITOR="C:\Program Files (x86)\Notepad++\notepad++.exe" -multiInst -nosession
> repo upload

Change-Id: Ic95b00f7443982b1956a2992d0220e50b1cf6bbb
2017-08-31 13:49:49 -07:00
010fed7711 Replace all os.remove calls
os.remove raises an exception when deleting read-only files on
Windows. Replace all calls with calls to platform_utils.remove,
which deals with deals with that issue.

Change-Id: I4dc9e0c9a36b4238880520c69f5075eca40f3e66
2017-08-31 13:49:36 -07:00
e8595e9df7 Support pager on Windows
Windows does not support pipe|fork, but we can simulate by creating
the pager as a child process, redirecting stdout/in/err appropriately
and then waiting for the child process to terminate after we are
done executing the repo command.

Change-Id: I5dd2bdeb4095e4d93bc678802e53c6d4eda0235b
2017-08-31 13:49:26 -07:00
227ad2ef42 Implement islink, readlink and realpath using Win32 api
Change-Id: I18452cbb32d24db73601ad10485dbe6bb278731c
2017-08-31 13:49:01 -07:00
2a4be94878 Handle Windows line endings when reading binary files
Without this change, '.git\HEAD' files, for examples, are sometime
read incorrectly resulting in the current branch to be reset to
"master" when running a "repo init -b xxx" on an already initialized
repository.

Change-Id: I48c7ef85ff81626edf156914329a560e14252f2a
2017-08-31 12:13:52 -07:00
9d743397bf Merge "Fixed upload to remotes with the url ssh://hostname" 2017-08-30 15:11:22 +00:00
2c57d619bc Merge "Add option '--no-cert-checks' for 'upload' sub command." 2017-08-30 15:11:10 +00:00
d1ebc89a08 Merge changes from topic "windows-support"
* changes:
  Port os.rename calls to work on Windows
  Workaround shutil.rmtree limitation on Windows
  Add support for creating symbolic links on Windows
  Make "git command" and "forall" work on Windows
2017-08-30 10:24:03 +00:00
2ec2a5d64c Fixed upload to remotes with the url ssh://hostname
Change-Id: I1d0dd4d3f90eac45205f6f4ca98a29b0babdbc3f
2017-08-29 20:16:08 +00:00
9ead97bb51 When starting a branch, do not use a tag or change value for branch.merge
When starting a branch, branch.merge is set to project revision unless
the revision is a SHA1. In that case, branch.merge is set to dest_branch
if defined or manifest default revision otherwise. This special handling
allows repo upload to work when the project revision is a SHA1.

Extend the special handling to also happen when the project revision
is a tag value or a change value so that repo upload will work in those
case as well.

Change-Id: Iff81ece40e770cd02535e80dcb023564d42dcf47
2017-08-25 09:10:29 +09:00
e43322625a Print a message when fetching is skipped for an immutable ref
The output indicates that fetching happens even when it is skipped.

To avoid confusion, print a message when fetching is skipped for
an immutable ref so that the user knows when and why a fetch is skipped.

Change-Id: Id6e4812cebc5e57d379feb76a9d034af0b93043b
2017-08-25 00:00:02 +00:00
bed59cec5e Add option '--no-cert-checks' for 'upload' sub command.
This option allow to bypass verification ssl certification while
establishing connection with Gerrit to upload review.

Change-Id: If2e15f5a273c18a700eb5093ca8a4d5a4cbf80cd
2017-08-23 14:06:14 +02:00
c94d6eb902 Revert "Migrate git-repo to create private changes rather than drafts"
This reverts commit d88f53e2b9. I merged
it too hastily without paying enough attention to compatibility with
released Gerrit versions.

Change-Id: I4028d4737df1255f11e217da183a19a010597d5b
2017-08-08 18:34:53 +00:00
d88f53e2b9 Migrate git-repo to create private changes rather than drafts
Considering that some users might expect changes created with
'-d' option are not public. Private changes may be a better
choice here than work-in-progress changes.

Change-Id: I46a8fb9ae38beb41cf96d6abe82bea6db2439669
2017-08-07 15:08:18 +02:00
87984c6db4 Add options for git-repo to support private and wip changes
This change adds options for git-repo tool to support private
changes and work-in-progress changes.

Change-Id: I343491f5949f06f1580d53f9cc0dee2dca09130f
2017-08-07 15:02:39 +02:00
ffc1401327 Merge "download: try to choose . as default project if none" 2017-08-02 07:19:45 +00:00
8a6eeed7f5 Merge "Always print percentage when syncing quietly" 2017-08-02 07:01:08 +00:00
7be072efa6 Always print percentage when syncing quietly
Change-Id: I574396e63520781067ed1e991c41caf7640e5731
2017-07-15 16:44:55 +00:00
7482a96443 download: try to choose . as default project if none
Change-Id: I28b5e3be5f3c9a4c077af87d6a3e0cc3b96a1b9d
2017-07-12 10:15:06 +02:00
3bcd30545e Fix "list comprehension redefines 'x'" warnings from pyflakes
$ git ls-files | grep py$ | xargs pyflakes
  subcmds/stage.py:101: list comprehension redefines 'p' from line 63
  subcmds/sync.py:784: list comprehension redefines 'p' from line 664
  subcmds/upload.py:467: list comprehension redefines 'avail' from line 454

Change-Id: Ia65d1a72ed185ab3357e1a91ed4450c719e75a7c
2017-07-10 23:26:04 +00:00
224a31a765 init: add missing submodule arg
The submodule argument to Sync_LocalHalf was missing in
MetaBranchSwitch, causing submodules not to get synced when the
-b/--manifest-branch argument to init is used.

Change-Id: Ie86d271abac2020725770be36ead83be3326e64b
Signed-off-by: Martin Kelly <mkelly@xevo.com>
2017-07-10 14:50:52 -07:00
b54343d9fd Tell the user if it will upload a draft
Change-Id: Ie004ec9d61603f3f618c47597947b82c59f2839c
2017-07-10 13:20:01 +00:00
259f16520a Merge "Add a newline after "Fetching projects" progress output" 2017-06-28 04:24:03 +00:00
8419ab22d6 sync: Continue job if some fetchs failed but force-broken is set
With --force-broken it continue to fetch other projects but nothing
is added in directory because it abort some lines later.

Change-Id: I32c4a4619b3028893dc4f98e8d4e5bc5c09adb27
2017-06-16 12:23:26 +02:00
913327f10c Add a newline after "Fetching projects" progress output
Output before change:

    Fetching project platform/packages/providers/UserDictionaryProvider
    Fetching projects:  66% (773/1171)  Fetching project platform/external/regex-re2
    Fetching project device/generic/mini-emulator-x86_64

Output after change:

    Fetching project platform/packages/providers/UserDictionaryProvider
    Fetching projects:  66% (773/1171)
    Fetching project platform/external/regex-re2
    Fetching project device/generic/mini-emulator-x86_64

Change-Id: I4da84da58316c69294e4da2792f83885dc942701
2017-06-13 13:03:39 +02:00
ad1abcb556 Port os.rename calls to work on Windows
os.rename fails on Windows if the destination exists, so replace
os.rename to platform_utils.rename which handles the platform
differences.

Change-Id: I15a86f10f65eedee5b003b80f88a0c28a3e1aa48
2017-05-29 19:33:07 +09:00
a65adf74f9 Workaround shutil.rmtree limitation on Windows
By default, shutil.rmtree raises an exception when deleting readonly
files on Windows.

Replace all shutil.rmtree with platform_utils.rmtree, which adds an
error handler to make files read-write when they can't be deleted.

Change-Id: I9cfea9a7b3703fb16a82cf69331540c2c179ed53
2017-05-29 19:32:31 +09:00
d5cec5e752 Add support for creating symbolic links on Windows
Replace all calls to os.symlink with platform_utils.symlink.

The Windows implementation calls into the CreateSymbolicLinkW Win32
API, as os.symlink is not supported.

Separate the Win32 API definitions into a separate module
platform_utils_win32 for clarity.

Change-Id: I0714c598664c2df93383734e609d948692c17ec5
2017-05-29 19:30:34 +09:00
2e70291162 Make "git command" and "forall" work on Windows
Python on Windows does not support non blocking file operations.
To workaround this issue, we instead use Threads and a Queue to
simulate non-blocking calls. This is happens only when running
with the native Windows version of Python, meaning Linux and Cygwin
are not affected by this change.

Change-Id: I4ce23827b096c5138f67a85c721f58a12279bb6f
2017-05-29 19:29:30 +09:00
35d22217a5 Ensure repo waits for child process to terminate
See http://stackoverflow.com/questions/7004687/os-exec-on-windows:

execv on Windows does not behave as on Linux, i.e. a new process is
spawned and the parent process terminates right away, which makes the
shell prompt come back too soon.

Change-Id: I1f8d23208765988629f081e9b949c67cf71c08ae
2017-05-29 13:56:18 +09:00
a24671f661 Merge "init: allow relative path on --reference argument" 2017-05-29 04:54:12 +00:00
e0684addee sync: Add support to dump a JSON event log of all sync events.
Change-Id: Id4852968ac1b2bf0093007cf2e5ca951ddab8b3b
2017-05-29 13:39:54 +09:00
fef9f21b28 Fix misplaced file separator string.replace call
Project names are stored as path using the '/' file separator, and
stored in a dictionary as keys.

Change-Id: Ide40dfe840958ac0d46caae5f77f1a49d71c9d90
2017-05-28 21:18:13 +09:00
6a470be220 Use OS file separator
Change-Id: I46b15bc1c1b4f2300a6fd98fe16c755da4910e7a
2017-05-28 21:16:15 +09:00
169d8ae93c Merge "pre-auto-gc: Add support for Windows" 2017-05-28 00:37:59 +00:00
c79d3b8fd1 init: allow relative path on --reference argument
Change-Id: I41d6be6bc035fdddb5a27c072994439986d58d58
Signed-off-by: YOUNG HO CHA <ganadist@gmail.com>
2017-05-28 00:33:25 +00:00
aa90021fbc Set result if sys.exit() is called by subcommand.
Allows the finally branch to make sure of the return code.

Change-Id: I7a796da5b60269cbd71aad953f1b9bb762b8eef8
2017-05-27 13:32:00 +09:00
fddfa6fbac Adding include element into the top-level element
The documentation of the XML file format contains DTD which contains
definition of all allowed elements and attributes. The "include" element
is defined but it's not referenced from the top-level "manifest"
element.

This patch is adding the "include" element into the list of elements of
the top-level "manifest" element.

Change-Id: I33beb8ef2846bbf42ffd42e6ae6888828566d604
2017-05-27 04:26:15 +00:00
997a92bd58 Merge "Add option REPO_IGNORE_SSH_INFO to ignore ssh_info" 2017-05-27 04:25:39 +00:00
fbcbcabe98 Merge "init: add --submodules to sync manifest submodules" 2017-05-27 04:24:58 +00:00
eec726c6d8 Add option REPO_IGNORE_SSH_INFO to ignore ssh_info
This is required for setups, where Gerrit access using ssh is only available
for some networks.
For network without ssh access, repo will get ssh_info from Gerrit and
use ssh for communications - which will fail. To support this setup
we need to have an option to ignore the ssh_info provided by Gerrit and
use http(s).

Using git insteadOf as alternative results in the inability to add
reviewers using "repo upload --re=...", since the syntax of adding
reviewers differs for ssh and https. repo is assuming an ssh
connection and uses "git push --receive-pack=...", which will fail
since git silently uses https for push operation. repo must be aware
that https is used so it uses "git push remote ...:refs/for/...%r=..."
for upload.

Change-Id: Idd83baef0fb26ffcc9ac65e204b68d323ce177a1
2017-05-26 15:11:11 +02:00
666debc518 gitc_delete: Remove unused imports
Change-Id: I672189ba99e18dca3956e2396c921d1ef0ca2ddd
2017-05-26 21:53:34 +09:00
c354a9b922 abandon: fix usage of undefined variable
As reported by pyflakes:

  subcmds/abandon.py:84: undefined name 'p'

The name of the variable should be 'proj'.

Change-Id: Ic09eb92e8db6b510e99efce010bd0bb094d7cbfe
2017-05-26 21:52:23 +09:00
06848b2415 Update .mailmap
Change-Id: Icabc7fb2161b661c2df9290a1ca6f75b9b1b8e1b
2017-05-26 21:44:57 +09:00
e4e94d26ae init: add --submodules to sync manifest submodules
repo sync can sync submodules via the --fetch-submodules option.
However, if the manifest repo has submodules, those will not be synced.
Having submodules in the manifest repo -- while not commonly done -- can
be useful for inheriting a manifest from another project using <include>
and layering changes on top of it.  In this way, you can avoid having to
deal with merge conflicts between your own manifests and the other
project's manifests (for example, if you're managing an Android fork).

Add a --submodule option to init that automatically syncs the submodules
in the manifest repo whenever the manifest repo changes.

Change-Id: I45d34f04517774c1462d7f233f482d1d81a332a8
Signed-off-by: Martin Kelly <mkelly@xevo.com>
2017-05-23 16:51:31 -07:00
c9439facdd pre-auto-gc: Add support for Windows
Previously, this would always have exited with 1 on Windows, causing "git
gc --auto" to abort. Fix this by adding support for Windows.

Change-Id: Ie519b366a11b6b18b2d465e892e738de3f4bbc99
2017-05-03 11:26:21 +00:00
3d7bbc9edf project.py: fix performance issue with --reference when the mirrored repository has many refs
Change-Id: Id0183903597f872eee80ca32a8050125b187a3d4
2017-04-12 20:04:47 +08:00
ffb4b89099 sync.py: report the remote URL on fatal git remote errors
repo can be configured to download from any number of remote git repos.
However when one fails repo doesn't report which one. Example:
Fatal: remote error: Daily ls-remote rate limit exceeded for IP xx.xx.xx.xx

TEST=repo init -q -u https://chromium.googlesource.com/chromiumos/manifest.git
  # Apply patch in ./.repo/repo/
  # Simulate a git remote error:
  sed -i -e 's#chromiumos/docs#chromiumos/XXdocs#' .repo/manifests/full.xml
  repo sync --quiet --force-sync docs
  # error message now shows the remote URL

Optional test tip: reduce the time.sleep(random(...)) in ./.repo/repo/project.py

Change-Id: I4509383b6a43a8e66064778e8ed612d8a735c8b6
2017-04-04 22:10:34 -07:00
04071c1c72 manifest-format: fix EMPTY keyword usage
The keyword EMPTY doesn't use parens.

BUG=git-repo:140

Change-Id: I7cd28a09c401520a72e5c244a77d9d70385f1b61
2016-12-28 16:07:16 -05:00
7de8c5db78 Merge "init: Add no-tags and current branch options" 2016-12-09 02:33:42 +00:00
bb9c42cf1d Merge "Fix removing broken symlink in reference dir" 2016-12-06 07:51:01 +00:00
f4dda9a1be init: Add no-tags and current branch options
This avoids fetching tags and branches for huge manifests

Change-Id: I19c9724d75364440b881b297d42b906f541f73ff
2016-12-01 19:03:41 -05:00
b881d227f3 Merge "Add a check and more output to protect against invalid REPO_URLs" 2016-10-29 07:28:35 +00:00
8e2d1d521e Merge "Fix checkout error when depth passed to repo init and revision is a sha1" 2016-10-28 19:30:45 +00:00
27226e742d Add a check and more output to protect against invalid REPO_URLs
If you don't know that the url to git-repo itself can be overridden via
REPO_URL, it's hard to debug cases where REPO_URL is accidentally set to
another repository, e.g. inside a Jenkins CI job. What makes is even
harder is that the ".repo/repo" directory gets silently removed in such
cases as verifications fails, which makes it impossible to look at the
cloned files to understand the problem.

To better protect against such an issue, warn if the cloned git-repo
repository does not contain a top-level "repo" file, and state that the
".repo/repo" directory will be removed in case of a clone failure.

Change-Id: I697b4999205a5967910c0237772ccaada01e74d4
2016-10-28 14:43:02 +02:00
6c5944606a Fix checkout error when depth passed to repo init and revision is a sha1
Currently, if direct fetch of a sha1 is not supported by git server and
depth option is used, we fallback on syncing the upstream branch by
ignoring depth option.

This fallback doesn't work in next 2 cases:
(1) upstream attribute is not specified in manifest
(2) depth option is passed to repo init command
    (not with clone-depth attribute in manifest)

This commit do the following:
- fixes (1) by updating condition used to apply fallback
  first we retry with depth set to None, then by syncing all branches
- fixes (2) by passing depth as argument of _RemoteFetch() method
  thus, its value is not set again to depth value passed to repo init
  command when applying fallback

Change-Id: Ifd6fffafc49ba229df624b0d7b64c83d47619d17
2016-10-28 14:29:57 +02:00
ae81c964b6 Merge "implement optional '--all' in the abandon command" 2016-10-28 07:52:31 +00:00
e02c17c9ea Merge "_CheckDirReference: log actual error before suggesting --force-sync" 2016-10-28 06:49:07 +00:00
6e31079033 Add sso to list of known schemes for relative URLs
repo already special-cases sso:// URLs to behave similarly to https://
and rpc:// elsewhere in repo, but it forgot to do so here.

Noticed when trying to use relative URLs in a manifest obtained using
an sso:// URL.

Change-Id: Ia11469a09bbd6e444dbc4f22c82f9bbe9f5fd083
2016-10-27 15:56:38 -07:00
ec287902e6 _CheckDirReference: log actual error before suggesting --force-sync
A recent backward incompatible change created confusion and loss of
productivity and highlighted the very limited amount of information
provided when repo sync fails; merely recommending to --force-sync
and blow-up git repos without any hint as to why. The addition of
this basic _error(...) call would have provided a clue and will in
the future.

BUG=Issue 232
TEST=simulate a breakage similar to the ones reported at
  https://groups.google.com/a/chromium.org/forum/#!topic/chromium-os-dev/2-0oCy_CX5s
  cd .repo/projects/src/third_party/libapps.git/
  file info; rm info; ln -s wronglink info
  cd -
  repo sync src/third_party/libapps/
  # error message now shows the failure

Change-Id: Idd2f177a096f1ad686caa8c67cb361d594ccaa57
2016-10-27 12:58:26 -07:00
4d5bb68d58 status: add -q/--quiet option
The --quiet option reduces the output to just
a list of projects with modified workspaces (and
orphans if -o is specified)

A common use case is when performing a full-workspace
merge.  The integrator will kick-off a merge via:

    repo forall -c git merge <some tag>

And then produce a short list of conflicted projects via:

    repo status -q

The integrator can then iteratively fix and clean up all conficted
components.  The merge is complete when:

    repo status -q

    returns no output.

Change-Id: Ibbba8713eac35befd8287c95948874e23fd5c7e2
2016-10-17 15:24:09 -05:00
2e14792a94 implement optional '--all' in the abandon command
when you want to delete all local branches, you should be find
all branches' name, and type them behind 'repo abandon' command.

Usage:
    repo abandon --all [<project>...]

Change-Id: I4d391f37fb9d89b8095488c585468eafc1a35f31
2016-10-17 02:29:42 +00:00
82f67987a3 Merge "sync: Fix semaphore release bug that causes thread 'leaks'" 2016-10-17 01:10:16 +00:00
699bcd40be Removed duplication code in abandon.py
code about getting argument is duplicated.
so this line is removed

Change-Id: Id321b999c7dacdb403cd986cbf35f8db62efc157
2016-10-12 10:02:30 +09:00
7f1ccfbb7b sync: Fix semaphore release bug that causes thread 'leaks'
When repo syncs a manifest that utilizes multiple branches
in the same project, then the sync will use an extra
thread for each "duplicate".  For example, if
the manifest includes the project "foo" and "bar"
twice, then "repo sync -jN" will fetch with N+2 threads.

This is caused by _FetchHelper() releasing the thread semaphore
object each time it's called, even though _FetchProjectList()
may call this function multiple times within the scope of a
single thread.

Fix by moving the thread semaphore release to
_FetchProjectList(), which is only called once per thread
instance.

Change-Id: I1da78b145e09524d40457db5ca5c37d315432bd8
2016-10-11 14:10:34 -05:00
eceeb1b1f5 Support broken symlinks when cleaning obsolete paths
When there's a symlink to a directory, os.walk still lists the symlink
in dirs, even if it isn't configured to follow symlinks. This will fail
the listdirs check if the symlink is broken (either before or during the
cleanup). So instead, check for directory symlinks and remove them using
os.remove.

Bug: Issue 231
Change-Id: I0ec45a26be566613a4a39bf694a3d9c6328481c2
2016-09-27 03:05:11 +00:00
16889ba43d Revert "Repo: fall back to http, if ssh connection fails for http repos"
This reverts commit 488bf092d5.

Issue 230

Change-Id: I3a5725301f576e1a2ac499cb6daa631895115640
2016-09-22 16:40:27 +00:00
40d3952270 Merge "On project cleanup, don't remove nested projects" 2016-09-21 22:23:44 +00:00
4350791e0d On project cleanup, don't remove nested projects
When there are nested projects in a manifest, like on AOSP right now:

<project path="build" name="platform/build" />
<project path="build/blueprint" name="platform/build/blueprint" />
<project path="build/kati" name="platform/build/kati" />
<project path="build/soong" name="platform/build/soong" />

And the top "build" project is removed (or renamed to remove the
nesting), repo just wipes away everything under build/ and re-creates
the projects that are still there. But it only checks to see if the
build/ project is dirty, so if there are dirty files in a nested
project, they'll just be blown away, and a fresh worktree checked out.

Instead, behave similarly to how `git clean -dxf` behaves and preserve
any subdirectories that have git repositories in them. This isn't as
strict as git -- it does not check to see if the '.git' entry is a
readable gitdir, just whether an entry named '.git' exists.

If it encounters any errors removing files, we'll print them all out to
stderr and tell the user that we were unable to clean up the obsolete
project, that they should clean it up manually, then sync again.

Change-Id: I2f6a7dd205a8e0b7590ca5369e9b0ba21d5a6f77
2016-09-20 17:16:12 -07:00
d648045366 implement optional 'pushurl' in the manifest file
Allow the 'remote' element in the manifest file to define an optional
'pushurl' attribute which is passed into the .git/config file.

Change-Id: If342d299d371374aedc4440645798888869c9714
Signed-off-by: Steve Rae <steve.rae@raedomain.com>
2016-09-20 15:31:20 +00:00
628456833a Merge "Repo: improve error detection for new ssh connections" 2016-09-20 08:06:12 +00:00
2aa61d0bc8 Merge "Repo: fall back to http, if ssh connection fails for http repos" 2016-09-20 08:05:57 +00:00
4aed6f8c7d Merge "Replace pylint with pyflakes/flake8" 2016-09-20 06:57:16 +00:00
01b7d758d5 Merge "When syncing a project with a shared object store, disable automatic pruning." 2016-09-14 20:48:24 +00:00
267ac57361 Merge "repo: add comment for updating maintainer keys" 2016-09-14 14:06:42 +00:00
bb5b1a076b Replace pylint with pyflakes/flake8
pylint reports a lot of warnings, but many of them are false positive,
and it's difficult to configure it. It also seems that for some reason
the included config file is not working well with the latest version.

Update the documentation to recommend using pyflakes and flake8 instead
of pylint. Remove the pylint config and add a basic flake8 config with
minimum settings:

- Maximum line length 80 columns
- Ignore warnings about indentation (repo uses 2 rather than expected 4)
- Ignore warnings about import placement

In this commit no code cleanup is done, and it's expected that most of
the files will throw up quite a few warnings, at least for flake8. These
can be cleaned up in follow-up commits.

The existing pylint suppression comments are left as-is. These will be
helpful when cleaning up pyflakes warnings later.

Change-Id: I2f7cb4340266ed07cc973ca6483b8f09d66a765b
2016-09-14 09:49:02 +02:00
e01ee026e6 Merge "Fix submodule checkout error when using sync-s option" 2016-09-14 07:45:50 +00:00
e4433653db repo: add comment for updating maintainer keys
Change-Id: Ic1e7557f9597234033561ab9fb3104b87e30015e
2016-09-14 01:28:30 -04:00
d9de945d8a Merge "upload: short circuit when nothing is pending" 2016-09-14 05:20:36 +00:00
2ff302929c When syncing a project with a shared object store, disable automatic pruning.
The shared object stores confuse git and make it throw away objects which are
still in use. We'll avoid that problem by disabling automatic pruning on those
projects, but there's nothing preventing a user from changing the config back
or pruning a repository manually.

BUG=chromium:375945
TEST=Ran repo sync on fresh ChromeOS checkout, starting with a branch of repo
with this change. Verified that the kernel projects and no others were
identified as having shared object stores, and that repo successfully disabled
automatic pruning in their configs. Re-enabled pruning and ran repo sync just
on one of the kernel directories. Verified that pruning was re-disabled as a
result.

Change-Id: I728ed5b06f0087aeb5a23ba8f5410a7cd10af5b0
2016-09-14 00:19:44 -04:00
e5c0ea0a95 Consider local project to be default for 'repo start'
The requirement to explicitly specify the local project when starting
a new repo branch is somewhat counter intuitive.

This patch uses the current directory's git tree as the default
project.

Tested by running

  'repo start <name>'

observed that the result is the same as if running

  'repo start <name> .'

Change-Id: If106caa801b4cd5ba70dbe8354a227d59f100aa3
2016-09-14 00:17:45 -04:00
163a3be18b upload: short circuit when nothing is pending
When nothing is pending, most of this code is already short-circuited.
Hoist the single check up to make this more obvious/slightly faster.

Change-Id: Iec3a7e08eacd23a7c5f964900d5776bf5252c804
2016-09-14 00:16:37 -04:00
7a77c16d37 Update mailmap
Order the entries alphabetically and add a couple more.

Change-Id: I8d98e8a5a1dd6868b566a42428030d2040b8af7a
2016-09-02 11:12:28 +09:00
488bf092d5 Repo: fall back to http, if ssh connection fails for http repos
if a gerrit server has ssh and https access enabled, but user access
(for some users) is limited to https, 'repo upload' command will fail
for them.

Gerrit returns a ssh configuration (gerrit/ssh_info), that does not work
for users limited to https.

With this patch repo will test, if the returned ssh configuration from
gerrit/ssh_info is working. if not, it will fall back to https for upload.

Change-Id: If98f472e994f350bf71f35610cd649b163f1ab33
2016-08-30 07:35:03 +00:00
05dc46b0e3 Repo: improve error detection for new ssh connections
this check can only detect errors that happen within 1 sec after launching
ssh. But this is typically enough to catch configuration issues like
'connection refused' or 'authentication failed'.

Change-Id: I00b6f62d4c2889b1faa6c820e49a198554c92795
2016-08-30 07:34:33 +00:00
39252ba028 gitc: Lower concurrent ls-projects requests
Too many requests at the same time is causing 502 errors.

Change-Id: Ic8fbb2fbb7fb6014733fa5be018d2dc02472f704
2016-08-23 14:19:00 -07:00
71e4cea6de RepoHook: do not list options twice during hash based approval
Instead of

  Do you want to allow this script to run (yes/yes-never-ask-again/NO)? (yes/always/NO)?

ask

  Do you want to allow this script to run (yes/always/NO)?

Change-Id: I5f5a2d0e88086a8d85e54fb8623a62d74a20956a
Signed-off-by: Jonathan Nieder <jrn@google.com>
2016-08-18 17:06:26 -07:00
c4c2b066d1 Increment the wrapper version
There have been a number of changes in the repo wrapper since the last
increment that was done in fee390ee:

- 9711a98 init: Add --no-clone-bundle option
- 631d0ec Support non-ASCII GNUPGHOME environment variable
- 4088eb4 repo: Cleaned up pylint/pep8 violations
- 5553628 repo: Add check of REPO_URL env variable
- 745b4ad Fix gitc-init behavior
- d3ddcdb Ignore clone.bundle on HTTP 501, i.e. Not Implemented

Change-Id: I3f763ef0ec2df2d726dff429021b48ad474148f1
2016-08-17 13:58:17 +09:00
6a0a3648f1 Merge "init: Add --no-clone-bundle option" 2016-08-17 04:57:28 +00:00
6118faa118 Merge "init: Respect --quiet option when synching manifest repository" 2016-08-17 04:04:14 +00:00
183c52ab02 Merge "project: Set config option to skip lfs smudge filter" 2016-08-17 01:00:12 +00:00
58f85f9a30 Merge "RepoHook: allow users to approve hooks via manifests" 2016-08-16 18:05:23 +00:00
40252c20f7 RepoHook: allow users to approve hooks via manifests
The constant prompting when registered hooks change can be tedious and
has a large multiplication factor when the project is large (e.g. the
AOSP).  It gets worse as people want to write more checks, hooks, docs,
and tests (or fix bugs), but every CL that goes in will trigger a new
prompt to approve.

Let's tweak our trust model when it comes to hooks.  Since people start
off by calling `repo init` with a URL to a manifest, and that manifest
defines all the hooks, anchor trust in that.  This requires that we get
the manifest over a trusted link (e.g. https or ssh) so that it can't
be MITM-ed.  If the user chooses to use an untrusted link (e.g. git or
http), then we'll fallback to the existing hash based approval.

Bug: Issue 226
Change-Id: I77be9e4397383f264fcdaefb582e345ea4069a13
2016-08-16 13:02:52 -04:00
76a4a9df86 project: Set config option to skip lfs smudge filter
During sync, repo runs `git read-tree --reset -u -v HEAD` which causes
git-lfs's smudge filter to run. However this fails because git-lfs does
not work with bare repositories.

Add lfs.filter configuration to the project config as suggested in the
comments on the upstream git-lfs client issue [1]. This prevents the
smudge filter from running, and the sync completes successfully.

For any projects that have LFS objects, `git lfs pull` must be executed.

[1] https://github.com/github/git-lfs/issues/1422

Bug: Issue 224
Change-Id: I091ff37998131e2e6bbc59aa37ee352fe12d7fcd
2016-08-16 21:55:36 +09:00
befaec1e56 improve docs
Change-Id: Ide4008f09c2f17f8fb3d85dfffe94544abfdd6a6
2016-08-16 00:14:28 -04:00
9711a98d6c init: Add --no-clone-bundle option
Bug: Issue 218
Change-Id: I42ba1f5fb9168875da0df6bdf4fe44c8d6498d54
2016-08-15 09:51:48 +09:00
438eade413 init: Respect --quiet option when synching manifest repository
Change-Id: Ib58b7dd971670e0888e6428333050700e776b0de
2016-08-15 09:51:48 +09:00
69297c1b77 Merge "Support non-ASCII GNUPGHOME environment variable" 2016-08-15 00:51:32 +00:00
8016f60a46 Merge "repo: Repo does not always handle '.' parameter correctly" 2016-08-14 08:50:28 +00:00
631d0ec708 Support non-ASCII GNUPGHOME environment variable
Here we don't need to encode this gpg_dir string when using
Python 2.7 on Linux.

Change-Id: I56724e9511d3b1aea61535e654a45c212130630d
2016-07-16 22:10:06 +03:00
f97e72e5dd Bail out when manifest is referencing a bad SHA-1 revision.
BUG: Issue 222
Change-Id: Ie0a64b39922d6fdf1be2989eb514985be8490278
2016-06-29 11:01:43 -07:00
8ac0c96537 Fix removing broken symlink in reference dir
Re-ordered to first create the symlink before checking the source
file and remove the destination if the source does not exists.

Change-Id: Iae923ba2ef0ba5a8dc1b8e42d8cc3f3708f773af
2016-06-29 05:43:01 +00:00
faaddc9b4e Merge "Adds additional crlf clobber avoidance." 2016-06-29 05:42:20 +00:00
a36af0767b Merge "Fix variable assignment" 2016-06-29 01:43:41 +00:00
037040f73e Fix variable assignment
If upstream string is empty, current_branch_only variable will be assigned
to an empty string.

This is not what we expect here as this variable is a boolean.

Change-Id: Ibba935e25a74c2be1e50c88b4b403cf394ba365e
2016-06-28 12:31:25 +02:00
2598ed06f1 Fix submodule checkout error when using sync-s option
When sync-s="true" option is used, the checkout of a submodule will try
to use the revision attribute of the parent project.

If this revision is a named reference, the checkout will fail if there
is no reference with this name in the submodule.

The proposed solution is to use the git commit id as revisionExpr for
submodules.

Change-Id: Ie8390a11957fd6a9c61289c6861d13cb3fa11678
2016-06-27 20:16:45 +02:00
01952e6634 Adds additional crlf clobber avoidance.
Adds the hook-scripts to .gitattributes due to the shell-scripts not
liking CRLF which they will get if a user sets 'autocrlf = true'
in their global gitconfig.

Further, since the python interpreter can handle either CRLF or LF, 
python-scripts specific line-ending rules have been removed.

Change-Id: I2d6bfd491b2f626b9ca93c40a3a7f2cfba6c54f0
2016-06-22 08:36:45 +00:00
9d2b14d2ec pylint: Fix unused-{argument,variable} warning
This commit fixes 4 out of the remaining 5 pylint warnings:

$ pylint --rcfile=.pylintrc *.py
************* Module gitc_utils
W:146, 0: TODO(sbasi/jorg): Come up with a solution to remove the sleep below. (fixme)
W:130, 6: Unused variable 'name' (unused-variable)
************* Module main
W:382,32: Unused argument 'fp' (unused-argument)
W:382,36: Unused argument 'code' (unused-argument)
W:382,42: Unused argument 'msg' (unused-argument)

Change-Id: Ie3d77b9a65b7daefaa9aa4b80f4b682c1678fd58
Signed-off-by: Stefan Beller <sbeller@google.com>
2016-06-21 11:48:57 -07:00
6685106306 pylint: fix indentation in manifest_xml
This fixes pylint warning:

************* Module manifest_xml
W:975, 0: Bad indentation. Found 8 spaces, expected 6 (bad-indentation)

Change-Id: I967212f9439430351836ebdc27e442d7b77476e2
Signed-off-by: Stefan Beller <sbeller@google.com>
2016-06-17 16:45:48 -07:00
d64e8eee51 pylint: ignore bad-whitespace
This ignores whitespaces errors, which we have quite a few of in argument
lists, for example:

************* Module git_config
C:209, 0: No space allowed around keyword argument assignment
  def HasSection(self, section, subsection = ''):
                                           ^ (bad-whitespace)
C:320, 0: No space allowed around keyword argument assignment
                   capture_stdout = True,
                                  ^ (bad-whitespace)
C:321, 0: No space allowed around keyword argument assignment
                   capture_stderr = True)
                                  ^ (bad-whitespace)
C:427, 0: Exactly one space required after comma
                     '-o','ControlPath %s' % ssh_sock(),
                         ^ (bad-whitespace)
C:436, 0: Exactly one space required after comma
    check_command = command_base + ['-O','check']
                                        ^ (bad-whitespace)
C:464, 0: Exactly one space required after comma
             % (host,port, str(e)), file=sys.stderr)
                    ^ (bad-whitespace)
C:707, 0: No space allowed around keyword argument assignment
    return self._config.GetString(key, all_keys = all_keys)
                                                ^ (bad-whitespace)
C:759, 0: No space allowed around keyword argument assignment
    return self._config.GetString(key, all_keys = all_keys)
                                                ^ (bad-whitespace)

Change-Id: Ia8f154f6741ce609787551f65877d7584c457903
Signed-off-by: Stefan Beller <sbeller@google.com>
2016-06-17 16:37:24 -07:00
8b39fb4bc0 Merge "Fix XmlManifest.Save with remotes that have 'alias' set" 2016-04-22 18:28:57 +00:00
96c2d65489 Fix XmlManifest.Save with remotes that have 'alias' set
When the alias attribute is set for a remote, the RemoteSpec attached to
a Project only contains the alias name used by git, not the original
name used in the manifest. But that's not enough information to
reconstruct the manifest, so save off the original manifest name as
another RemoteSpec parameter, only used to write the manifest out.

Bug: Issue 181
Bug: Issue 219
Change-Id: Id7417dfd6ce5572e4e5fe14f22924fdf088ca4f3
2016-04-22 10:32:06 +09:00
7ecccf6225 diffmanifests: support custom git pretty format strings
Change-Id: I29f4f1351c421f393328514d145df1a96aed9ee2
2016-04-21 18:36:11 +00:00
cee5c77166 Add a .mailmap file
Change-Id: I3c7e68fae0f8c082b2e0fbfc26cfb7dda31f1d34
2016-04-18 19:08:23 +09:00
79fba68e40 sync: Update help text for --smart-sync to be more specific
The --smart-sync option should return the manifest for *the latest*
known good build.

Change-Id: I2f3216b5b9e1af2ea5f9c3bf1c025813a3b77581
2016-04-13 18:03:00 +09:00
e868841782 Improve documentation of manifest server RPC methods
Mention that the RPC endpoints are used when running repo
sync with the --smart-sync and --smart-tag options

Change-Id: I4b0b82e8b714fe923a5b325a6135f0128bf636ff
2016-04-13 17:55:36 +09:00
f9fe3e14d2 repo: Repo does not always handle '.' parameter correctly
The repo script allows a manifest to specify a '.' as the path the
top-level directory, which co-locates the .git and .repo directories,
and places files from the git repository at the top-level:

  <project name="proj_name" path="." />
  <project name="sierra.other.git" path="other" />

Most commands work correctly with this setup. Some commands, however,
fail to find the project. For instance, 'repo sync' works, and 'repo sync .'
works in a sub-project ('other' in this case) but 'repo sync .' in the
top-level directory fails with the error:

error: project . not found

There are two reasons for this:

1. The self.worktree attribute of the Project object is not normalized,
so with a '.' for path its value would be '/my/project/root/.'. This is
fine when used as a path, since it's the same path as '/my/project/root',
but when used in a string comparison it fails. This commit applies
os.path.normpath() to that value before storing it.

2. The _GetProjectByPath method in command.py was not checking the path
against manifest.topdir, so even once it was normalized the project was
not found. This commit adds a check against manifest.topdir if the
loop drops out without finding a project.

Change-Id: Ic84d053f1bbb5a357cad566805d5a326ae8246d2
2016-04-08 00:07:52 +00:00
bdb866ea76 Fix symlinking of new projects
We weren't copying these lists, so the += was actually changing the
underlying lists.

When a new project was added to the manifest, we run _CheckDirReference
against the manifest project with share_refs=True, which added the
working_tree_* to the shareable_* lists. Then, when we load the new
manifest and create the new project, it uses the lists that already
contain the working_tree_* files, even though we passed
share_refs=False.

This happens reliably under the above conditions, but doesn't seem to
happen when syncing a fresh tree. So we've got a mixture of links that
may need to be cleaned up later. This patch will just stop it from
happening in the future.

Change-Id: Ib7935bfad78af1e494a75e55134ec829f13c2a41
2016-04-05 17:44:09 -07:00
e121ad558d Merge "Ignore clone.bundle on HTTP 501, i.e. Not Implemented" 2016-04-05 21:39:29 +00:00
1f0564406b Add --inverse-regex option to forall subcommand
Make it possible to exclude projects using regex/wildcard.

The syntax is similar to that of the -r option, e.g.:

  repo forall -i ^platform/ ^device/ -c 'echo $REPO_PROJECT'

Change-Id: Id250de5665152228c044c79337d3ac15b5696484
2016-04-05 07:28:27 +00:00
936d6185eb Merge changes from topic 'pylint-pep8-cleanup'
* changes:
  command.py: Cleaned up pylint/pep8 violations
  project.py: Cleaned up pylint/pep8 violations
2016-03-15 00:31:12 +00:00
9322964d14 Update commit-msg hook to version from Gerrit 2.12.1
Change-Id: I31b74aba998f8e83f370a759218777f2557a8872
2016-03-14 10:08:33 +09:00
4aa4b211c6 RepoHook: set __file__ when running the hook
A common design pattern is to use __file__ to find the location of the
active python module to assist in output or loading of related assets.
The current hook systems runs the pre-upload.py hook in a context w/out
that set leading to runtime errors:

$ repo upload --cbr .
ERROR: Traceback (most recent call last):
  File ".../repo/project.py", line 481, in _ExecuteHook
    self._script_fullpath, 'exec'), context)
  File ".../repohooks/pre-upload.py", line 32, in <module>
    path = os.path.dirname(os.path.realpath(__file__))
NameError: name '__file__' is not defined

Define this variable in this context so code can safely use it.

Change-Id: If6331312445fa61d9351b59f83abcc1c99ae6748
2016-03-05 21:52:31 +00:00
8ccfa74d12 command.py: Cleaned up pylint/pep8 violations
I noticed when running pylint (as the SUBMITTING_PATCHES file directs)
that there were a few violations reported. This makes it difficult
to see violations I might have introduced. This commit corrects all
pylint violations in the command.py script.

This script now has a pylint score of 10.0.

Change-Id: Ibb35fa9af0e0b9b40e02ae043682b3af23286748
2016-03-02 09:05:45 -07:00
30b0f4e022 project.py: Cleaned up pylint/pep8 violations
I noticed when running pylint (as the SUBMITTING_PATCHES file directs)
that there were a number of violations reported. This makes it difficult
to see violations I might have introduced. This commit corrects all
pylint violations in the project.py script.

This script now has a pylint score of 10.0, and no violations reported
by pep8.

Change-Id: I1462fd84f5b6b4c0dc893052671373e7ffd838f1
2016-03-02 09:05:40 -07:00
d3ddcdbd8a Ignore clone.bundle on HTTP 501, i.e. Not Implemented
Change-Id: I03ee003d3bd5d0684a31bdf7961a55a511dfa0e2
2015-08-12 20:12:51 +02:00
68 changed files with 3608 additions and 1382 deletions

3
.flake8 Normal file
View File

@ -0,0 +1,3 @@
[flake8]
max-line-length=80
ignore=E111,E114,E402

2
.gitattributes vendored
View File

@ -1,4 +1,4 @@
# Prevent /bin/sh scripts from being clobbered by autocrlf=true # Prevent /bin/sh scripts from being clobbered by autocrlf=true
git_ssh text eol=lf git_ssh text eol=lf
main.py text eol=lf
repo text eol=lf repo text eol=lf
hooks/* text eol=lf

12
.mailmap Normal file
View File

@ -0,0 +1,12 @@
Anthony Newnam <anthony.newnam@garmin.com> Anthony <anthony@bnovc.com>
He Ping <tdihp@hotmail.com> heping <tdihp@hotmail.com>
Hu Xiuyun <xiuyun.hu@hisilicon.com> Hu xiuyun <xiuyun.hu@hisilicon.com>
Hu Xiuyun <xiuyun.hu@hisilicon.com> Hu Xiuyun <clouds08@qq.com>
Jelly Chen <chenguodong@huawei.com> chenguodong <chenguodong@huawei.com>
Jia Bi <bijia@xiaomi.com> bijia <bijia@xiaomi.com>
JoonCheol Park <jooncheol@gmail.com> Jooncheol Park <jooncheol@gmail.com>
Sergii Pylypenko <x.pelya.x@gmail.com> pelya <x.pelya.x@gmail.com>
Shawn Pearce <sop@google.com> Shawn O. Pearce <sop@google.com>
Ulrik Sjölin <ulrik.sjolin@sonyericsson.com> Ulrik Sjolin <ulrik.sjolin@gmail.com>
Ulrik Sjölin <ulrik.sjolin@sonyericsson.com> Ulrik Sjolin <ulrik.sjolin@sonyericsson.com>
Ulrik Sjölin <ulrik.sjolin@sonyericsson.com> Ulrik Sjölin <ulrik.sjolin@sonyericsson.com>

View File

@ -5,6 +5,6 @@
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH"> <pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
<path>/git-repo</path> <path>/git-repo</path>
</pydev_pathproperty> </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_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
</pydev_project> </pydev_project>

298
.pylintrc
View File

@ -1,298 +0,0 @@
# lint Python modules using external checkers.
#
# This is the main checker controling the other ones and the reports
# generation. It is itself both a raw checker and an astng checker in order
# to:
# * handle message activation / deactivation at the module level
# * handle some basic but necessary stats'data (number of classes, methods...)
#
[MASTER]
# Specify a configuration file.
#rcfile=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Profiled execution.
profile=no
# Add <file or directory> to the black list. It should be a base name, not a
# path. You may set this option multiple times.
ignore=SVN
# Pickle collected data for later comparisons.
persistent=yes
# Set the cache size for astng objects.
cache-size=500
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
[MESSAGES CONTROL]
# Enable only checker(s) with the given id(s). This option conflicts with the
# disable-checker option
#enable-checker=
# Enable all checker(s) except those with the given id(s). This option
# conflicts with the enable-checker option
#disable-checker=
# Enable all messages in the listed categories.
#enable-msg-cat=
# Disable all messages in the listed categories.
#disable-msg-cat=
# Enable the message(s) with the given id(s).
enable=RP0004
# Disable the message(s) with the given id(s).
disable=R0903,R0912,R0913,R0914,R0915,W0141,C0111,C0103,W0603,W0703,R0911,C0301,C0302,R0902,R0904,W0142,W0212,E1101,E1103,R0201,W0201,W0122,W0232,RP0001,RP0003,RP0101,RP0002,RP0401,RP0701,RP0801,F0401,E0611,R0801,I0011
[REPORTS]
# set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html
output-format=text
# Put messages in a separate file for each module / package specified on the
# command line instead of printing them on stdout. Reports (if any) will be
# written in a file name "pylint_global.[txt|html]".
files-output=no
# Tells whether to display a full report or only the messages
reports=yes
# Python expression which should return a note less than 10 (10 is the highest
# note).You have access to the variables errors warning, statement which
# respectivly contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (R0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Add a comment according to your evaluation note. This is used by the global
# evaluation report (R0004).
comment=no
# checks for
# * unused variables / imports
# * undefined variables
# * redefinition of variable from builtins or from an outer scope
# * use of variable before assigment
#
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching names used for dummy variables (i.e. not used).
dummy-variables-rgx=_|dummy
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
# try to find bugs in the code using type inference
#
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamicaly set).
ignored-classes=SQLObject
# When zope mode is activated, consider the acquired-members option to ignore
# access to some undefined attributes.
zope=no
# List of members which are usually get through zope's acquisition mecanism and
# so shouldn't trigger E0201 when accessed (need zope=yes to be considered).
acquired-members=REQUEST,acl_users,aq_parent
# checks for :
# * doc strings
# * modules / classes / functions / methods / arguments / variables name
# * number of arguments, local variables, branchs, returns and statements in
# functions, methods
# * required module attributes
# * dangerous default values as arguments
# * redefinition of function / method / class
# * uses of the global statement
#
[BASIC]
# Required attributes for module, separated by a comma
required-attributes=
# Regular expression which should only match functions or classes name which do
# not require a docstring
no-docstring-rgx=_main|__.*__
# Regular expression which should only match correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression which should only match correct module level names
const-rgx=(([A-Z_][A-Z1-9_]*)|(__.*__))|(log)$
# Regular expression which should only match correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Regular expression which should only match correct function names
function-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct method names
method-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct instance attribute names
attr-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct argument names
argument-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct variable names
variable-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct list comprehension /
# generator expression variable names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_,e,d1,d2,v,f,l,d
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# List of builtins function names that should not be used, separated by a comma
bad-functions=map,filter,apply,input
# checks for sign of poor/misdesign:
# * number of methods, attributes, local variables...
# * size, complexity of functions, methods
#
[DESIGN]
# Maximum number of arguments for function / method
max-args=5
# Maximum number of locals for function / method body
max-locals=15
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of branch for function / method body
max-branchs=12
# Maximum number of statements in function / method body
max-statements=50
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of attributes for a class (see R0902).
max-attributes=20
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
# Maximum number of public methods for a class (see R0904).
max-public-methods=30
# checks for
# * external modules dependencies
# * relative / wildcard imports
# * cyclic imports
# * uses of deprecated modules
#
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report R0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report R0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in the given file (report R0402 must
# not be disabled)
int-import-graph=
# checks for :
# * methods without self as first argument
# * overridden methods signature
# * access only to existant members via self
# * attributes not defined in the __init__ method
# * supported interfaces implementation
# * unreachable code
#
[CLASSES]
# List of interface methods to ignore, separated by a comma. This is used for
# instance to not check methods defines in Zope's Interface base class.
ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp
# checks for similarities and duplicated code. This computation may be
# memory / CPU intensive, so you should disable it if you experiments some
# problems.
#
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# checks for:
# * warning notes in the code like FIXME, XXX
# * PEP 263: source code with non ascii character but no encoding declaration
#
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO
# checks for :
# * unauthorized constructions
# * strict indentation
# * line length
# * use of <> instead of !=
#
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=80
# Maximum number of lines in a module
max-module-lines=1000
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab). In repo it is 2 spaces.
indent-string=' '

16
README.md Normal file
View File

@ -0,0 +1,16 @@
# repo
Repo is a tool built on top of Git. Repo helps manage many Git repositories,
does the uploads to revision control systems, and automates parts of the
development workflow. Repo is not meant to replace Git, only to make it
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://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)

View File

@ -1,17 +1,19 @@
Short Version: [TOC]
# Short Version
- Make small logical changes. - Make small logical changes.
- Provide a meaningful commit message. - Provide a meaningful commit message.
- Check for coding errors with pylint - Check for coding errors and style nits with pyflakes and flake8
- Make sure all code is under the Apache License, 2.0. - Make sure all code is under the Apache License, 2.0.
- Publish your changes for review. - Publish your changes for review.
- Make corrections if requested. - Make corrections if requested.
- Verify your changes on gerrit so they can be submitted. - Verify your changes on gerrit so they can be submitted.
git push https://gerrit-review.googlesource.com/git-repo HEAD:refs/for/master `git push https://gerrit-review.googlesource.com/git-repo HEAD:refs/for/master`
Long Version: # Long Version
I wanted a file describing how to submit patches for repo, I wanted a file describing how to submit patches for repo,
so I started with the one found in the core Git distribution so I started with the one found in the core Git distribution
@ -19,10 +21,10 @@ so I started with the one found in the core Git distribution
patch submission guidelines for the Linux kernel. patch submission guidelines for the Linux kernel.
However there are some differences, so please review and familiarize However there are some differences, so please review and familiarize
yourself with the following relevant bits: yourself with the following relevant bits.
(1) Make separate commits for logically separate changes. ## Make separate commits for logically separate changes.
Unless your patch is really trivial, you should not be sending Unless your patch is really trivial, you should not be sending
out a patch that was generated between your working tree and your out a patch that was generated between your working tree and your
@ -36,14 +38,46 @@ If your description starts to get too long, that's a sign that you
probably need to split up your commit to finer grained pieces. probably need to split up your commit to finer grained pieces.
(2) Check for coding errors with pylint ## Check for coding errors and style nits with pyflakes and flake8
Run pylint on changed modules using the provided configuration: ### Coding errors
pylint --rcfile=.pylintrc file.py Run `pyflakes` on changed modules:
pyflakes file.py
Ideally there should be no new errors or warnings introduced.
### Style violations
Run `flake8` on changes modules:
flake8 file.py
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/
(3) Check the license ## Running tests
There is a [`./run_tests`](./run_tests) helper script for quickly invoking all
of our unittests. 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. repo is licensed under the Apache License, 2.0.
@ -59,7 +93,7 @@ your patch. It is virtually impossible to remove a patch once it
has been applied and pushed out. has been applied and pushed out.
(4) Sending your patches. ## Sending your patches.
Do not email your patches to anyone. Do not email your patches to anyone.
@ -91,23 +125,23 @@ to get the ChangeId added.
Push your patches over HTTPS to the review server, possibly through Push your patches over HTTPS to the review server, possibly through
a remembered remote to make this easier in the future: a remembered remote to make this easier in the future:
git config remote.review.url https://gerrit-review.googlesource.com/git-repo git config remote.review.url https://gerrit-review.googlesource.com/git-repo
git config remote.review.push HEAD:refs/for/master git config remote.review.push HEAD:refs/for/master
git push review git push review
You will be automatically emailed a copy of your commits, and any You will be automatically emailed a copy of your commits, and any
comments made by the project maintainers. comments made by the project maintainers.
(5) Make changes if requested ## Make changes if requested
The project maintainer who reviews your changes might request changes to your The project maintainer who reviews your changes might request changes to your
commit. If you make the requested changes you will need to amend your commit commit. If you make the requested changes you will need to amend your commit
and push it to the review server again. and push it to the review server again.
(6) Verify your changes on gerrit ## Verify your changes on gerrit
After you receive a Code-Review+2 from the maintainer, select the Verified After you receive a Code-Review+2 from the maintainer, select the Verified
button on the gerrit page for the change. This verifies that you have tested button on the gerrit page for the change. This verifies that you have tested

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2008 The Android Open Source Project # 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 # Copyright (C) 2008 The Android Open Source Project
# #
@ -19,6 +20,7 @@ import platform
import re import re
import sys import sys
from event_log import EventLog
from error import NoSuchProjectError from error import NoSuchProjectError
from error import InvalidProjectGroupsError from error import InvalidProjectGroupsError
@ -28,10 +30,11 @@ class Command(object):
""" """
common = False common = False
event_log = EventLog()
manifest = None manifest = None
_optparse = None _optparse = None
def WantPager(self, opt): def WantPager(self, _opt):
return False return False
def ReadEnvironmentOptions(self, opts): def ReadEnvironmentOptions(self, opts):
@ -63,7 +66,7 @@ class Command(object):
usage = self.helpUsage.strip().replace('%prog', me) usage = self.helpUsage.strip().replace('%prog', me)
except AttributeError: except AttributeError:
usage = 'repo %s' % self.NAME usage = 'repo %s' % self.NAME
self._optparse = optparse.OptionParser(usage = usage) self._optparse = optparse.OptionParser(usage=usage)
self._Options(self._optparse) self._Options(self._optparse)
return self._optparse return self._optparse
@ -95,6 +98,16 @@ class Command(object):
self.OptionParser.print_usage() self.OptionParser.print_usage()
sys.exit(1) 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): def Execute(self, opt, args):
"""Perform the action, after option parsing is complete. """Perform the action, after option parsing is complete.
""" """
@ -110,15 +123,20 @@ class Command(object):
project = None project = None
if os.path.exists(path): if os.path.exists(path):
oldpath = None oldpath = None
while path \ while path and \
and path != oldpath \ path != oldpath and \
and path != manifest.topdir: path != manifest.topdir:
try: try:
project = self._by_path[path] project = self._by_path[path]
break break
except KeyError: except KeyError:
oldpath = path oldpath = path
path = os.path.dirname(path) path = os.path.dirname(path)
if not project and path == manifest.topdir:
try:
project = self._by_path[path]
except KeyError:
pass
else: else:
try: try:
project = self._by_path[path] project = self._by_path[path]
@ -138,7 +156,7 @@ class Command(object):
mp = manifest.manifestProject mp = manifest.manifestProject
if not groups: if not groups:
groups = mp.config.GetString('manifest.groups') groups = mp.config.GetString('manifest.groups')
if not groups: if not groups:
groups = 'default,platform-' + platform.system().lower() groups = 'default,platform-' + platform.system().lower()
groups = [x for x in re.split(r'[,\s]+', groups) if x] groups = [x for x in re.split(r'[,\s]+', groups) if x]
@ -151,8 +169,7 @@ class Command(object):
for p in project.GetDerivedSubprojects()) for p in project.GetDerivedSubprojects())
all_projects_list.extend(derived_projects.values()) all_projects_list.extend(derived_projects.values())
for project in all_projects_list: for project in all_projects_list:
if ((missing_ok or project.Exists) and if (missing_ok or project.Exists) and project.MatchesGroups(groups):
project.MatchesGroups(groups)):
result.append(project) result.append(project)
else: else:
self._ResetPathToProjectMap(all_projects_list) self._ResetPathToProjectMap(all_projects_list)
@ -166,8 +183,8 @@ class Command(object):
# If it's not a derived project, update path->project mapping and # If it's not a derived project, update path->project mapping and
# search again, as arg might actually point to a derived subproject. # search again, as arg might actually point to a derived subproject.
if (project and not project.Derived and if (project and not project.Derived and (submodules_ok or
(submodules_ok or project.sync_s)): project.sync_s)):
search_again = False search_again = False
for subproject in project.GetDerivedSubprojects(): for subproject in project.GetDerivedSubprojects():
self._UpdatePathToProjectMap(subproject) self._UpdatePathToProjectMap(subproject)
@ -194,48 +211,52 @@ class Command(object):
result.sort(key=_getpath) result.sort(key=_getpath)
return result return result
def FindProjects(self, args): def FindProjects(self, args, inverse=False):
result = [] result = []
patterns = [re.compile(r'%s' % a, re.IGNORECASE) for a in args] patterns = [re.compile(r'%s' % a, re.IGNORECASE) for a in args]
for project in self.GetProjects(''): for project in self.GetProjects(''):
for pattern in patterns: for pattern in patterns:
if pattern.search(project.name) or pattern.search(project.relpath): match = pattern.search(project.name) or pattern.search(project.relpath)
if not inverse and match:
result.append(project) result.append(project)
break break
if inverse and match:
break
else:
if inverse:
result.append(project)
result.sort(key=lambda project: project.relpath) result.sort(key=lambda project: project.relpath)
return result return result
# pylint: disable=W0223
# Pylint warns that the `InteractiveCommand` and `PagedCommand` classes do not
# override method `Execute` which is abstract in `Command`. Since that method
# is always implemented in classes derived from `InteractiveCommand` and
# `PagedCommand`, this warning can be suppressed.
class InteractiveCommand(Command): class InteractiveCommand(Command):
"""Command which requires user interaction on the tty and """Command which requires user interaction on the tty and
must not run within a pager, even if the user asks to. must not run within a pager, even if the user asks to.
""" """
def WantPager(self, opt): def WantPager(self, _opt):
return False return False
class PagedCommand(Command): class PagedCommand(Command):
"""Command which defaults to output in a pager, as its """Command which defaults to output in a pager, as its
display tends to be larger than one screen full. display tends to be larger than one screen full.
""" """
def WantPager(self, opt): def WantPager(self, _opt):
return True return True
# pylint: enable=W0223
class MirrorSafeCommand(object): class MirrorSafeCommand(object):
"""Command permits itself to run within a mirror, """Command permits itself to run within a mirror,
and does not require a working directory. and does not require a working directory.
""" """
class GitcAvailableCommand(object): class GitcAvailableCommand(object):
"""Command that requires GITC to be available, but does """Command that requires GITC to be available, but does
not require the local client to be a GITC client. not require the local client to be a GITC client.
""" """
class GitcClientCommand(object): class GitcClientCommand(object):
"""Command that requires the local client to be a GITC """Command that requires the local client to be a GITC
client. client.

View File

@ -1,110 +1,116 @@
repo Manifest Format # repo Manifest Format
====================
A repo manifest describes the structure of a repo client; that is A repo manifest describes the structure of a repo client; that is
the directories that are visible and where they should be obtained the directories that are visible and where they should be obtained
from with git. from with git.
The basic structure of a manifest is a bare Git repository holding The basic structure of a manifest is a bare Git repository holding
a single 'default.xml' XML file in the top level directory. a single `default.xml` XML file in the top level directory.
Manifests are inherently version controlled, since they are kept Manifests are inherently version controlled, since they are kept
within a Git repository. Updates to manifests are automatically within a Git repository. Updates to manifests are automatically
obtained by clients during `repo sync`. obtained by clients during `repo sync`.
[TOC]
XML File Format
---------------
A manifest XML file (e.g. 'default.xml') roughly conforms to the ## XML File Format
A manifest XML file (e.g. `default.xml`) roughly conforms to the
following DTD: following DTD:
<!DOCTYPE manifest [ ```xml
<!ELEMENT manifest (notice?, <!DOCTYPE manifest [
remote*, <!ELEMENT manifest (notice?,
default?, remote*,
manifest-server?, default?,
remove-project*, manifest-server?,
project*, remove-project*,
extend-project*, project*,
repo-hooks?)> extend-project*,
repo-hooks?,
include*)>
<!ELEMENT notice (#PCDATA)> <!ELEMENT notice (#PCDATA)>
<!ELEMENT remote (EMPTY)> <!ELEMENT remote EMPTY>
<!ATTLIST remote name ID #REQUIRED> <!ATTLIST remote name ID #REQUIRED>
<!ATTLIST remote alias CDATA #IMPLIED> <!ATTLIST remote alias CDATA #IMPLIED>
<!ATTLIST remote fetch CDATA #REQUIRED> <!ATTLIST remote fetch CDATA #REQUIRED>
<!ATTLIST remote review CDATA #IMPLIED> <!ATTLIST remote pushurl CDATA #IMPLIED>
<!ATTLIST remote revision CDATA #IMPLIED> <!ATTLIST remote review CDATA #IMPLIED>
<!ATTLIST remote revision CDATA #IMPLIED>
<!ELEMENT default (EMPTY)> <!ELEMENT default EMPTY>
<!ATTLIST default remote IDREF #IMPLIED> <!ATTLIST default remote IDREF #IMPLIED>
<!ATTLIST default revision CDATA #IMPLIED> <!ATTLIST default revision CDATA #IMPLIED>
<!ATTLIST default dest-branch CDATA #IMPLIED> <!ATTLIST default dest-branch CDATA #IMPLIED>
<!ATTLIST default sync-j CDATA #IMPLIED> <!ATTLIST default upstream CDATA #IMPLIED>
<!ATTLIST default sync-c CDATA #IMPLIED> <!ATTLIST default sync-j CDATA #IMPLIED>
<!ATTLIST default sync-s CDATA #IMPLIED> <!ATTLIST default sync-c CDATA #IMPLIED>
<!ATTLIST default sync-s CDATA #IMPLIED>
<!ATTLIST default sync-tags CDATA #IMPLIED>
<!ELEMENT manifest-server (EMPTY)> <!ELEMENT manifest-server EMPTY>
<!ATTLIST manifest-server url CDATA #REQUIRED> <!ATTLIST manifest-server url CDATA #REQUIRED>
<!ELEMENT project (annotation*, <!ELEMENT project (annotation*,
project*, project*,
copyfile*, copyfile*,
linkfile*)> linkfile*)>
<!ATTLIST project name CDATA #REQUIRED> <!ATTLIST project name CDATA #REQUIRED>
<!ATTLIST project path CDATA #IMPLIED> <!ATTLIST project path CDATA #IMPLIED>
<!ATTLIST project remote IDREF #IMPLIED> <!ATTLIST project remote IDREF #IMPLIED>
<!ATTLIST project revision CDATA #IMPLIED> <!ATTLIST project revision CDATA #IMPLIED>
<!ATTLIST project dest-branch CDATA #IMPLIED> <!ATTLIST project dest-branch CDATA #IMPLIED>
<!ATTLIST project groups CDATA #IMPLIED> <!ATTLIST project groups CDATA #IMPLIED>
<!ATTLIST project sync-c CDATA #IMPLIED> <!ATTLIST project sync-c CDATA #IMPLIED>
<!ATTLIST project sync-s CDATA #IMPLIED> <!ATTLIST project sync-s CDATA #IMPLIED>
<!ATTLIST project upstream CDATA #IMPLIED> <!ATTLIST project sync-tags CDATA #IMPLIED>
<!ATTLIST project clone-depth CDATA #IMPLIED> <!ATTLIST project upstream CDATA #IMPLIED>
<!ATTLIST project force-path CDATA #IMPLIED> <!ATTLIST project clone-depth CDATA #IMPLIED>
<!ATTLIST project force-path CDATA #IMPLIED>
<!ELEMENT annotation (EMPTY)> <!ELEMENT annotation EMPTY>
<!ATTLIST annotation name CDATA #REQUIRED> <!ATTLIST annotation name CDATA #REQUIRED>
<!ATTLIST annotation value CDATA #REQUIRED> <!ATTLIST annotation value CDATA #REQUIRED>
<!ATTLIST annotation keep CDATA "true"> <!ATTLIST annotation keep CDATA "true">
<!ELEMENT copyfile (EMPTY)> <!ELEMENT copyfile EMPTY>
<!ATTLIST copyfile src CDATA #REQUIRED> <!ATTLIST copyfile src CDATA #REQUIRED>
<!ATTLIST copyfile dest CDATA #REQUIRED> <!ATTLIST copyfile dest CDATA #REQUIRED>
<!ELEMENT linkfile (EMPTY)> <!ELEMENT linkfile EMPTY>
<!ATTLIST linkfile src CDATA #REQUIRED> <!ATTLIST linkfile src CDATA #REQUIRED>
<!ATTLIST linkfile dest CDATA #REQUIRED> <!ATTLIST linkfile dest CDATA #REQUIRED>
<!ELEMENT extend-project (EMPTY)> <!ELEMENT extend-project EMPTY>
<!ATTLIST extend-project name CDATA #REQUIRED> <!ATTLIST extend-project name CDATA #REQUIRED>
<!ATTLIST extend-project path CDATA #IMPLIED> <!ATTLIST extend-project path CDATA #IMPLIED>
<!ATTLIST extend-project groups CDATA #IMPLIED> <!ATTLIST extend-project groups CDATA #IMPLIED>
<!ATTLIST extend-project revision CDATA #IMPLIED>
<!ELEMENT remove-project (EMPTY)> <!ELEMENT remove-project EMPTY>
<!ATTLIST remove-project name CDATA #REQUIRED> <!ATTLIST remove-project name CDATA #REQUIRED>
<!ELEMENT repo-hooks (EMPTY)> <!ELEMENT repo-hooks EMPTY>
<!ATTLIST repo-hooks in-project CDATA #REQUIRED> <!ATTLIST repo-hooks in-project CDATA #REQUIRED>
<!ATTLIST repo-hooks enabled-list CDATA #REQUIRED> <!ATTLIST repo-hooks enabled-list CDATA #REQUIRED>
<!ELEMENT include (EMPTY)> <!ELEMENT include EMPTY>
<!ATTLIST include name CDATA #REQUIRED> <!ATTLIST include name CDATA #REQUIRED>
]> ]>
```
A description of the elements and their attributes follows. A description of the elements and their attributes follows.
Element manifest ### Element manifest
----------------
The root element of the file. The root element of the file.
Element remote ### Element remote
--------------
One or more remote elements may be specified. Each remote element One or more remote elements may be specified. Each remote element
specifies a Git URL shared by one or more projects and (optionally) specifies a Git URL shared by one or more projects and (optionally)
@ -125,6 +131,12 @@ Attribute `fetch`: The Git URL prefix for all projects which use
this remote. Each project's name is appended to this prefix to this remote. Each project's name is appended to this prefix to
form the actual URL used to clone the project. form the actual URL used to clone the project.
Attribute `pushurl`: The Git "push" URL prefix for all projects
which use this remote. Each project's name is appended to this
prefix to form the actual URL used to "git push" the project.
This attribute is optional; if not specified then "git push"
will use the same URL as the `fetch` attribute.
Attribute `review`: Hostname of the Gerrit server where reviews Attribute `review`: Hostname of the Gerrit server where reviews
are uploaded to by `repo upload`. This attribute is optional; are uploaded to by `repo upload`. This attribute is optional;
if not specified then `repo upload` will not function. if not specified then `repo upload` will not function.
@ -133,8 +145,7 @@ Attribute `revision`: Name of a Git branch (e.g. `master` or
`refs/heads/master`). Remotes with their own revision will override `refs/heads/master`). Remotes with their own revision will override
the default revision. the default revision.
Element default ### Element default
---------------
At most one default element may be specified. Its remote and At most one default element may be specified. Its remote and
revision attributes are used when a project element does not revision attributes are used when a project element does not
@ -153,6 +164,11 @@ Project elements not setting their own `dest-branch` will inherit
this value. If this value is not set, projects will use `revision` this value. If this value is not set, projects will use `revision`
by default instead. by default instead.
Attribute `upstream`: Name of the Git ref in which a sha1
can be found. Used when syncing a revision locked manifest in
-c mode to avoid having to sync the entire ref space. Project elements
not setting their own `upstream` will inherit this value.
Attribute `sync-j`: Number of parallel jobs to use when synching. Attribute `sync-j`: Number of parallel jobs to use when synching.
Attribute `sync-c`: Set to true to only sync the given Git Attribute `sync-c`: Set to true to only sync the given Git
@ -162,9 +178,12 @@ their own will use this value.
Attribute `sync-s`: Set to true to also sync sub-projects. Attribute `sync-s`: Set to true to also sync sub-projects.
Attribute `sync-tags`: Set to false to only sync the given Git
branch (specified in the `revision` attribute) rather than
the other ref tags.
Element manifest-server
----------------------- ### Element manifest-server
At most one manifest-server may be specified. The url attribute At most one manifest-server may be specified. The url attribute
is used to specify the URL of a manifest server, which is an is used to specify the URL of a manifest server, which is an
@ -172,10 +191,11 @@ XML RPC service.
The manifest server should implement the following RPC methods: The manifest server should implement the following RPC methods:
GetApprovedManifest(branch, target) GetApprovedManifest(branch, target)
Return a manifest in which each project is pegged to a known good revision Return a manifest in which each project is pegged to a known good revision
for the current branch and target. for the current branch and target. This is used by repo sync when the
--smart-sync option is given.
The target to use is defined by environment variables TARGET_PRODUCT The target to use is defined by environment variables TARGET_PRODUCT
and TARGET_BUILD_VARIANT. These variables are used to create a string and TARGET_BUILD_VARIANT. These variables are used to create a string
@ -184,14 +204,14 @@ If one of those variables or both are not present, the program will call
GetApprovedManifest without the target parameter and the manifest server GetApprovedManifest without the target parameter and the manifest server
should choose a reasonable default target. should choose a reasonable default target.
GetManifest(tag) GetManifest(tag)
Return a manifest in which each project is pegged to the revision at Return a manifest in which each project is pegged to the revision at
the specified tag. the specified tag. This is used by repo sync when the --smart-tag option
is given.
Element project ### Element project
---------------
One or more project elements may be specified. Each element One or more project elements may be specified. Each element
describes a single Git repository to be cloned into the repo describes a single Git repository to be cloned into the repo
@ -204,7 +224,7 @@ Attribute `name`: A unique name for this project. The project's
name is appended onto its remote's fetch URL to generate the actual name is appended onto its remote's fetch URL to generate the actual
URL to configure the Git remote with. The URL gets formed as: URL to configure the Git remote with. The URL gets formed as:
${remote_fetch}/${project_name}.git ${remote_fetch}/${project_name}.git
where ${remote_fetch} is the remote's fetch attribute and where ${remote_fetch} is the remote's fetch attribute and
${project_name} is the project's name attribute. The suffix ".git" ${project_name} is the project's name attribute. The suffix ".git"
@ -268,8 +288,7 @@ rather than the `name` attribute. This attribute only applies to the
local mirrors syncing, it will be ignored when syncing the projects in a local mirrors syncing, it will be ignored when syncing the projects in a
client working directory. client working directory.
Element extend-project ### Element extend-project
----------------------
Modify the attributes of the named project. Modify the attributes of the named project.
@ -284,8 +303,10 @@ at the specified path, rather than all projects with the given name.
Attribute `groups`: List of additional groups to which this project Attribute `groups`: List of additional groups to which this project
belongs. Same syntax as the corresponding element of `project`. belongs. Same syntax as the corresponding element of `project`.
Element annotation Attribute `revision`: If specified, overrides the revision 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 Zero or more annotation elements may be specified as children of a
project element. Each element describes a name-value pair that will be project element. Each element describes a name-value pair that will be
@ -295,23 +316,36 @@ prefixed with REPO__. In addition, there is an optional attribute
"false". This attribute determines whether or not the annotation will "false". This attribute determines whether or not the annotation will
be kept when exported with the manifest subcommand. be kept when exported with the manifest subcommand.
Element copyfile ### Element copyfile
----------------
Zero or more copyfile elements may be specified as children of a Zero or more copyfile elements may be specified as children of a
project element. Each element describes a src-dest pair of files; project element. Each element describes a src-dest pair of files;
the "src" file will be copied to the "dest" place during 'repo sync' the "src" file will be copied to the "dest" place during `repo sync`
command. command.
"src" is project relative, "dest" is relative to the top of the tree.
Element linkfile "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 It's just like copyfile and runs at the same time as copyfile but
instead of copying it creates a symlink. instead of copying it creates a symlink.
Element remove-project The symlink is created at "dest" (relative to the top of the tree) and
---------------------- points to the path specified by "src".
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 Deletes the named project from the internal manifest table, possibly
allowing a subsequent project element in the same manifest file to allowing a subsequent project element in the same manifest file to
@ -321,8 +355,7 @@ This element is mostly useful in a local manifest file, where
the user can remove a project, and possibly replace it with their the user can remove a project, and possibly replace it with their
own definition. own definition.
Element include ### Element include
---------------
This element provides the capability of including another manifest This element provides the capability of including another manifest
file into the originating manifest. Normal rules apply for the file into the originating manifest. Normal rules apply for the
@ -332,26 +365,25 @@ Attribute `name`: the manifest to include, specified relative to
the manifest repository's root. the manifest repository's root.
Local Manifests ## Local Manifests
===============
Additional remotes and projects may be added through local manifest Additional remotes and projects may be added through local manifest
files stored in `$TOP_DIR/.repo/local_manifests/*.xml`. files stored in `$TOP_DIR/.repo/local_manifests/*.xml`.
For example: For example:
$ ls .repo/local_manifests $ ls .repo/local_manifests
local_manifest.xml local_manifest.xml
another_local_manifest.xml another_local_manifest.xml
$ cat .repo/local_manifests/local_manifest.xml $ cat .repo/local_manifests/local_manifest.xml
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<manifest> <manifest>
<project path="manifest" <project path="manifest"
name="tools/manifest" /> name="tools/manifest" />
<project path="platform-manifest" <project path="platform-manifest"
name="platform/manifest" /> name="platform/manifest" />
</manifest> </manifest>
Users may add projects to the local manifest(s) prior to a `repo sync` Users may add projects to the local manifest(s) prior to a `repo sync`
invocation, instructing repo to automatically download and manage invocation, instructing repo to automatically download and manage

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-1.14.
* Older versions of Python (e.g. v2.7) may use the legacy feature-frozen branch
based on repo-1.13.
## 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

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

@ -0,0 +1,167 @@
# 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
```
[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

135
docs/repo-hooks.md Normal file
View File

@ -0,0 +1,135 @@
# repo hooks
[TOC]
Repo provides a mechanism to hook specific stages of the runtime with custom
python modules. All the hooks live in one git project which is checked out by
the manifest (specified during `repo init`), and the manifest itself defines
which hooks are registered.
These are useful to run linters, check formatting, and run quick unittests
before allowing a step to proceed (e.g. before uploading a commit to Gerrit).
A complete example can be found in the Android project. It can be easily
re-used by any repo based project and is not specific to Android.<br>
https://android.googlesource.com/platform/tools/repohooks
## Approvals
When a hook is processed the first time, the user is prompted for approval.
We don't want to execute arbitrary code without explicit consent. For manifests
fetched via secure protocols (e.g. https://), the user is prompted once. For
insecure protocols (e.g. http://), the user is prompted whenever the registered
repohooks project is updated and a hook is triggered.
## Manifest Settings
For the full syntax, see the [repo manifest format](./manifest-format.md).
Here's a short example from
[Android](https://android.googlesource.com/platform/manifest/+/master/default.xml).
The `<project>` line checks out the repohooks git repo to the local
`tools/repohooks/` path. The `<repo-hooks>` line says to look in the project
with the name `platform/tools/repohooks` for hooks to run during the
`pre-upload` phase.
```xml
<project path="tools/repohooks" name="platform/tools/repohooks" />
<repo-hooks in-project="platform/tools/repohooks" enabled-list="pre-upload" />
```
## Source Layout
The repohooks git repo should have a python file with the same name as the hook.
So if you want to support the `pre-upload` hook, you'll need to create a file
named `pre-upload.py`. Repo will dynamically load that module when processing
the hook and then call the `main` function in it.
Hooks should have their `main` accept `**kwargs` for future compatibility.
## Runtime
Hook return values are ignored.
Any uncaught exceptions from the hook will cause the step to fail. This is
intended as a fallback safety check though rather than the normal flow. If
you want your hook to trigger a failure, it should call `sys.exit()` (after
displaying relevant diagnostics).
Output (stdout & stderr) are not filtered in any way. Hooks should generally
not be too verbose. A short summary is nice, and some status information when
long running operations occur, but long/verbose output should be used only if
the hook ultimately fails.
The hook runs from the top level of the repo client where the operation is
started.
For example, if the repo client is under `~/tree/`, then that is where the hook
runs, even if you ran repo in a git repository at `~/tree/src/foo/`, or in a
subdirectory of that git repository in `~/tree/src/foo/bar/`.
Hooks frequently start off by doing a `os.chdir` to the specific project they're
called on (see below) and then changing back to the original dir when they're
finished.
Python's `sys.path` is modified so that the top of repohooks directory comes
first. This should help simplify the hook logic to easily allow importing of
local modules.
Repo does not modify the state of the git checkout. This means that the hooks
might be running in a dirty git repo with many commits and checked out to the
latest one. If the hook wants to operate on specific git commits, it needs to
manually discover the list of pending commits, extract the diff/commit, and
then check it directly. Hooks should not normally modify the active git repo
(such as checking out a specific commit to run checks) without first prompting
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.
### pre-upload
This hook runs when people run `repo upload`.
The `pre-upload.py` file should be defined like:
```py
def main(project_list, worktree_list=None, **kwargs):
"""Main function invoked directly by repo.
We must use the name "main" as that is what repo requires.
Args:
project_list: List of projects to run on.
worktree_list: A list of directories. It should be the same length as
project_list, so that each entry in project_list matches with a
directory in worktree_list. If None, we will attempt to calculate
the directories automatically.
kwargs: Leave this here for forward-compatibility.
"""
```

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2008 The Android Open Source Project # Copyright (C) 2008 The Android Open Source Project
# #
@ -21,6 +22,7 @@ import subprocess
import tempfile import tempfile
from error import EditorError from error import EditorError
import platform_utils
class Editor(object): class Editor(object):
"""Manages the user's preferred text editor.""" """Manages the user's preferred text editor."""
@ -82,7 +84,12 @@ least one of these before using this command.""", file=sys.stderr)
os.close(fd) os.close(fd)
fd = None fd = None
if re.compile("^.*[$ \t'].*$").match(editor): if platform_utils.isWindows():
# Split on spaces, respecting quoted strings
import shlex
args = shlex.split(editor)
shell = False
elif re.compile("^.*[$ \t'].*$").match(editor):
args = [editor + ' "$@"', 'sh'] args = [editor + ' "$@"', 'sh']
shell = True shell = True
else: else:
@ -107,4 +114,4 @@ least one of these before using this command.""", file=sys.stderr)
finally: finally:
if fd: if fd:
os.close(fd) os.close(fd)
os.remove(path) platform_utils.remove(path)

View File

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

177
event_log.py Normal file
View File

@ -0,0 +1,177 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2017 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.
from __future__ import print_function
import json
import multiprocessing
TASK_COMMAND = 'command'
TASK_SYNC_NETWORK = 'sync-network'
TASK_SYNC_LOCAL = 'sync-local'
class EventLog(object):
"""Event log that records events that occurred during a repo invocation.
Events are written to the log as a consecutive JSON entries, one per line.
Each entry contains the following keys:
- id: A ('RepoOp', ID) tuple, suitable for storing in a datastore.
The ID is only unique for the invocation of the repo command.
- name: Name of the object being operated upon.
- task_name: The task that was performed.
- start: Timestamp of when the operation started.
- finish: Timestamp of when the operation finished.
- success: Boolean indicating if the operation was successful.
- try_count: A counter indicating the try count of this task.
Optionally:
- parent: A ('RepoOp', ID) tuple indicating the parent event for nested
events.
Valid task_names include:
- command: The invocation of a subcommand.
- sync-network: The network component of a sync command.
- sync-local: The local component of a sync command.
Specific tasks may include additional informational properties.
"""
def __init__(self):
"""Initializes the event log."""
self._log = []
self._parent = None
def Add(self, name, task_name, start, finish=None, success=None,
try_count=1, kind='RepoOp'):
"""Add an event to the log.
Args:
name: Name of the object being operated upon.
task_name: A sub-task that was performed for name.
start: Timestamp of when the operation started.
finish: Timestamp of when the operation finished.
success: Boolean indicating if the operation was successful.
try_count: A counter indicating the try count of this task.
kind: The kind of the object for the unique identifier.
Returns:
A dictionary of the event added to the log.
"""
event = {
'id': (kind, _NextEventId()),
'name': name,
'task_name': task_name,
'start_time': start,
'try': try_count,
}
if self._parent:
event['parent'] = self._parent['id']
if success is not None or finish is not None:
self.FinishEvent(event, finish, success)
self._log.append(event)
return event
def AddSync(self, project, task_name, start, finish, success):
"""Add a event to the log for a sync command.
Args:
project: Project being synced.
task_name: A sub-task that was performed for name.
One of (TASK_SYNC_NETWORK, TASK_SYNC_LOCAL)
start: Timestamp of when the operation started.
finish: Timestamp of when the operation finished.
success: Boolean indicating if the operation was successful.
Returns:
A dictionary of the event added to the log.
"""
event = self.Add(project.relpath, task_name, start, finish, success)
if event is not None:
event['project'] = project.name
if project.revisionExpr:
event['revision'] = project.revisionExpr
if project.remote.url:
event['project_url'] = project.remote.url
if project.remote.fetchUrl:
event['remote_url'] = project.remote.fetchUrl
try:
event['git_hash'] = project.GetCommitRevisionId()
except Exception:
pass
return event
def GetStatusString(self, success):
"""Converst a boolean success to a status string.
Args:
success: Boolean indicating if the operation was successful.
Returns:
status string.
"""
return 'pass' if success else 'fail'
def FinishEvent(self, event, finish, success):
"""Finishes an incomplete event.
Args:
event: An event that has been added to the log.
finish: Timestamp of when the operation finished.
success: Boolean indicating if the operation was successful.
Returns:
A dictionary of the event added to the log.
"""
event['status'] = self.GetStatusString(success)
event['finish_time'] = finish
return event
def SetParent(self, event):
"""Set a parent event for all new entities.
Args:
event: The event to use as a parent.
"""
self._parent = event
def Write(self, filename):
"""Writes the log out to a file.
Args:
filename: The file to write the log to.
"""
with open(filename, 'w+') as f:
for e in self._log:
json.dump(e, f, sort_keys=True)
f.write('\n')
# An integer id that is unique across this invocation of the program.
_EVENT_ID = multiprocessing.Value('i', 1)
def _NextEventId():
"""Helper function for grabbing the next unique id.
Returns:
A unique, to this invocation of the program, integer id.
"""
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 # Copyright (C) 2008 The Android Open Source Project
# #
@ -14,15 +15,15 @@
# limitations under the License. # limitations under the License.
from __future__ import print_function from __future__ import print_function
import fcntl
import os import os
import select
import sys import sys
import subprocess import subprocess
import tempfile import tempfile
from signal import SIGTERM from signal import SIGTERM
from error import GitError from error import GitError
from trace import REPO_TRACE, IsTrace, Trace import platform_utils
from repo_trace import REPO_TRACE, IsTrace, Trace
from wrapper import Wrapper from wrapper import Wrapper
GIT = 'git' GIT = 'git'
@ -78,33 +79,13 @@ def terminate_ssh_clients():
_git_version = None _git_version = None
class _sfd(object):
"""select file descriptor class"""
def __init__(self, fd, dest, std_name):
assert std_name in ('stdout', 'stderr')
self.fd = fd
self.dest = dest
self.std_name = std_name
def fileno(self):
return self.fd.fileno()
class _GitCall(object): 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): def version_tuple(self):
global _git_version global _git_version
if _git_version is None: if _git_version is None:
ver_str = git.version() _git_version = Wrapper().ParseGitVersion()
_git_version = Wrapper().ParseGitVersion(ver_str)
if _git_version is None: 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) sys.exit(1)
return _git_version return _git_version
@ -117,13 +98,15 @@ class _GitCall(object):
return fun return fun
git = _GitCall() git = _GitCall()
def git_require(min_version, fail=False): def git_require(min_version, fail=False, msg=''):
git_version = git.version_tuple() git_version = git.version_tuple()
if min_version <= git_version: if min_version <= git_version:
return True return True
if fail: if fail:
need = '.'.join(map(str, min_version)) 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) sys.exit(1)
return False return False
@ -162,6 +145,7 @@ class GitCommand(object):
if ssh_proxy: if ssh_proxy:
_setenv(env, 'REPO_SSH_SOCK', ssh_sock()) _setenv(env, 'REPO_SSH_SOCK', ssh_sock())
_setenv(env, 'GIT_SSH', _ssh_proxy()) _setenv(env, 'GIT_SSH', _ssh_proxy())
_setenv(env, 'GIT_SSH_VARIANT', 'ssh')
if 'http_proxy' in env and 'darwin' == sys.platform: if 'http_proxy' in env and 'darwin' == sys.platform:
s = "'http.proxy=%s'" % (env['http_proxy'],) s = "'http.proxy=%s'" % (env['http_proxy'],)
p = env.get('GIT_CONFIG_PARAMETERS') p = env.get('GIT_CONFIG_PARAMETERS')
@ -253,19 +237,16 @@ class GitCommand(object):
def _CaptureOutput(self): def _CaptureOutput(self):
p = self.process p = self.process
s_in = [_sfd(p.stdout, sys.stdout, 'stdout'), s_in = platform_utils.FileDescriptorStreams.create()
_sfd(p.stderr, sys.stderr, 'stderr')] s_in.add(p.stdout, sys.stdout, 'stdout')
s_in.add(p.stderr, sys.stderr, 'stderr')
self.stdout = '' self.stdout = ''
self.stderr = '' self.stderr = ''
for s in s_in: while not s_in.is_done:
flags = fcntl.fcntl(s.fd, fcntl.F_GETFL) in_ready = s_in.select()
fcntl.fcntl(s.fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
while s_in:
in_ready, _, _ = select.select(s_in, [], [])
for s in in_ready: for s in in_ready:
buf = s.fd.read(4096) buf = s.read()
if not buf: if not buf:
s_in.remove(s) s_in.remove(s)
continue continue

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2008 The Android Open Source Project # Copyright (C) 2008 The Android Open Source Project
# #
@ -20,6 +21,7 @@ import errno
import json import json
import os import os
import re import re
import ssl
import subprocess import subprocess
import sys import sys
try: try:
@ -41,7 +43,8 @@ else:
from signal import SIGTERM from signal import SIGTERM
from error import GitError, UploadError from error import GitError, UploadError
from trace import Trace import platform_utils
from repo_trace import Trace
if is_python3(): if is_python3():
from http.client import HTTPException from http.client import HTTPException
else: else:
@ -50,16 +53,24 @@ else:
from git_command import GitCommand from git_command import GitCommand
from git_command import ssh_sock from git_command import ssh_sock
from git_command import terminate_ssh_clients from git_command import terminate_ssh_clients
from git_refs import R_CHANGES, R_HEADS, R_TAGS
R_HEADS = 'refs/heads/'
R_TAGS = 'refs/tags/'
ID_RE = re.compile(r'^[0-9a-f]{40}$') ID_RE = re.compile(r'^[0-9a-f]{40}$')
REVIEW_CACHE = dict() REVIEW_CACHE = dict()
def IsChange(rev):
return rev.startswith(R_CHANGES)
def IsId(rev): def IsId(rev):
return ID_RE.match(rev) return ID_RE.match(rev)
def IsTag(rev):
return rev.startswith(R_TAGS)
def IsImmutable(rev):
return IsChange(rev) or IsId(rev) or IsTag(rev)
def _key(name): def _key(name):
parts = name.split('.') parts = name.split('.')
if len(parts) < 2: if len(parts) < 2:
@ -259,7 +270,7 @@ class GitConfig(object):
try: try:
if os.path.getmtime(self._json) \ if os.path.getmtime(self._json) \
<= os.path.getmtime(self.file): <= os.path.getmtime(self.file):
os.remove(self._json) platform_utils.remove(self._json)
return None return None
except OSError: except OSError:
return None return None
@ -271,7 +282,7 @@ class GitConfig(object):
finally: finally:
fd.close() fd.close()
except (IOError, ValueError): except (IOError, ValueError):
os.remove(self._json) platform_utils.remove(self._json)
return None return None
def _SaveJson(self, cache): def _SaveJson(self, cache):
@ -283,7 +294,7 @@ class GitConfig(object):
fd.close() fd.close()
except (IOError, TypeError): except (IOError, TypeError):
if os.path.exists(self._json): if os.path.exists(self._json):
os.remove(self._json) platform_utils.remove(self._json)
def _ReadGit(self): def _ReadGit(self):
""" """
@ -296,8 +307,9 @@ class GitConfig(object):
d = self._do('--null', '--list') d = self._do('--null', '--list')
if d is None: if d is None:
return c return c
for line in d.decode('utf-8').rstrip('\0').split('\0'): # pylint: disable=W1401 if not is_python3():
# Backslash is not anomalous d = d.decode('utf-8')
for line in d.rstrip('\0').split('\0'):
if '\n' in line: if '\n' in line:
key, val = line.split('\n', 1) key, val = line.split('\n', 1)
else: else:
@ -464,9 +476,13 @@ def _open_ssh(host, port=None):
% (host,port, str(e)), file=sys.stderr) % (host,port, str(e)), file=sys.stderr)
return False return False
time.sleep(1)
ssh_died = (p.poll() is not None)
if ssh_died:
return False
_master_processes.append(p) _master_processes.append(p)
_master_keys.add(key) _master_keys.add(key)
time.sleep(1)
return True return True
finally: finally:
_master_keys_lock.release() _master_keys_lock.release()
@ -488,7 +504,7 @@ def close_ssh():
d = ssh_sock(create=False) d = ssh_sock(create=False)
if d: if d:
try: try:
os.rmdir(os.path.dirname(d)) platform_utils.rmdir(os.path.dirname(d))
except OSError: except OSError:
pass pass
@ -520,7 +536,7 @@ def GetUrlCookieFile(url, quiet):
for line in p.stdout: for line in p.stdout:
line = line.strip() line = line.strip()
if line.startswith(cookieprefix): if line.startswith(cookieprefix):
cookiefile = line[len(cookieprefix):] cookiefile = os.path.expanduser(line[len(cookieprefix):])
if line.startswith(proxyprefix): if line.startswith(proxyprefix):
proxy = line[len(proxyprefix):] proxy = line[len(proxyprefix):]
# Leave subprocess open, as cookie file may be transient. # Leave subprocess open, as cookie file may be transient.
@ -539,7 +555,10 @@ def GetUrlCookieFile(url, quiet):
if e.errno == errno.ENOENT: if e.errno == errno.ENOENT:
pass # No persistent proxy. pass # No persistent proxy.
raise raise
yield GitConfig.ForUser().GetString('http.cookiefile'), None cookiefile = GitConfig.ForUser().GetString('http.cookiefile')
if cookiefile:
cookiefile = os.path.expanduser(cookiefile)
yield cookiefile, None
def _preconnect(url): def _preconnect(url):
m = URI_ALL.match(url) m = URI_ALL.match(url)
@ -568,6 +587,7 @@ class Remote(object):
self._config = config self._config = config
self.name = name self.name = name
self.url = self._Get('url') self.url = self._Get('url')
self.pushUrl = self._Get('pushurl')
self.review = self._Get('review') self.review = self._Get('review')
self.projectname = self._Get('projectname') self.projectname = self._Get('projectname')
self.fetch = list(map(RefSpec.FromString, self.fetch = list(map(RefSpec.FromString,
@ -599,7 +619,7 @@ class Remote(object):
connectionUrl = self._InsteadOf() connectionUrl = self._InsteadOf()
return _preconnect(connectionUrl) return _preconnect(connectionUrl)
def ReviewUrl(self, userEmail): def ReviewUrl(self, userEmail, validate_certs):
if self._review_url is None: if self._review_url is None:
if self.review is None: if self.review is None:
return None return None
@ -607,7 +627,7 @@ class Remote(object):
u = self.review u = self.review
if u.startswith('persistent-'): if u.startswith('persistent-'):
u = u[len('persistent-'):] u = u[len('persistent-'):]
if u.split(':')[0] not in ('http', 'https', 'sso'): if u.split(':')[0] not in ('http', 'https', 'sso', 'ssh'):
u = 'http://%s' % u u = 'http://%s' % u
if u.endswith('/Gerrit'): if u.endswith('/Gerrit'):
u = u[:len(u) - len('/Gerrit')] u = u[:len(u) - len('/Gerrit')]
@ -623,20 +643,28 @@ class Remote(object):
host, port = os.environ['REPO_HOST_PORT_INFO'].split() host, port = os.environ['REPO_HOST_PORT_INFO'].split()
self._review_url = self._SshReviewUrl(userEmail, host, port) self._review_url = self._SshReviewUrl(userEmail, host, port)
REVIEW_CACHE[u] = self._review_url REVIEW_CACHE[u] = self._review_url
elif u.startswith('sso:'): elif u.startswith('sso:') or u.startswith('ssh:'):
self._review_url = u # Assume it's right self._review_url = u # Assume it's right
REVIEW_CACHE[u] = self._review_url REVIEW_CACHE[u] = self._review_url
elif 'REPO_IGNORE_SSH_INFO' in os.environ:
self._review_url = http_url
REVIEW_CACHE[u] = self._review_url
else: else:
try: try:
info_url = u + 'ssh_info' info_url = u + 'ssh_info'
info = urllib.request.urlopen(info_url).read() if not validate_certs:
if info == 'NOT_AVAILABLE' or '<' in info: context = ssl._create_unverified_context()
info = urllib.request.urlopen(info_url, context=context).read()
else:
info = urllib.request.urlopen(info_url).read()
if info == b'NOT_AVAILABLE' or b'<' in info:
# If `info` contains '<', we assume the server gave us some sort # If `info` contains '<', we assume the server gave us some sort
# of HTML response back, like maybe a login page. # of HTML response back, like maybe a login page.
# #
# Assume HTTP if SSH is not enabled or ssh_info doesn't look right. # Assume HTTP if SSH is not enabled or ssh_info doesn't look right.
self._review_url = http_url self._review_url = http_url
else: else:
info = info.decode('utf-8')
host, port = info.split() host, port = info.split()
self._review_url = self._SshReviewUrl(userEmail, host, port) self._review_url = self._SshReviewUrl(userEmail, host, port)
except urllib.error.HTTPError as e: except urllib.error.HTTPError as e:
@ -671,7 +699,8 @@ class Remote(object):
if not rev.startswith(R_HEADS): if not rev.startswith(R_HEADS):
return rev 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): def WritesTo(self, ref):
"""True if the remote stores to the tracking ref. """True if the remote stores to the tracking ref.
@ -694,6 +723,10 @@ class Remote(object):
"""Save this remote to the configuration. """Save this remote to the configuration.
""" """
self._Set('url', self.url) self._Set('url', self.url)
if self.pushUrl is not None:
self._Set('pushurl', self.pushUrl + '/' + self.projectname)
else:
self._Set('pushurl', self.pushUrl)
self._Set('review', self.review) self._Set('review', self.review)
self._Set('projectname', self.projectname) self._Set('projectname', self.projectname)
self._Set('fetch', list(map(str, self.fetch))) self._Set('fetch', list(map(str, self.fetch)))

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2009 The Android Open Source Project # Copyright (C) 2009 The Android Open Source Project
# #
@ -14,13 +15,15 @@
# limitations under the License. # limitations under the License.
import os import os
from trace import Trace from repo_trace import Trace
import platform_utils
HEAD = 'HEAD' HEAD = 'HEAD'
R_HEADS = 'refs/heads/' R_CHANGES = 'refs/changes/'
R_TAGS = 'refs/tags/' R_HEADS = 'refs/heads/'
R_PUB = 'refs/published/' R_TAGS = 'refs/tags/'
R_M = 'refs/remotes/m/' R_PUB = 'refs/published/'
R_M = 'refs/remotes/m/'
class GitRefs(object): class GitRefs(object):
@ -126,9 +129,9 @@ class GitRefs(object):
def _ReadLoose(self, prefix): def _ReadLoose(self, prefix):
base = os.path.join(self._gitdir, prefix) base = os.path.join(self._gitdir, prefix)
for name in os.listdir(base): for name in platform_utils.listdir(base):
p = os.path.join(base, name) p = os.path.join(base, name)
if os.path.isdir(p): if platform_utils.isdir(p):
self._mtime[prefix] = os.path.getmtime(base) self._mtime[prefix] = os.path.getmtime(base)
self._ReadLoose(prefix + name + '/') self._ReadLoose(prefix + name + '/')
elif name.endswith('.lock'): elif name.endswith('.lock'):
@ -138,7 +141,7 @@ class GitRefs(object):
def _ReadLoose1(self, path, name): def _ReadLoose1(self, path, name):
try: try:
fd = open(path, 'rb') fd = open(path)
except IOError: except IOError:
return return

15
git_ssh
View File

@ -1,2 +1,17 @@
#!/bin/sh #!/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" "$@" 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 # Copyright (C) 2015 The Android Open Source Project
# #
@ -24,7 +25,9 @@ import git_command
import git_config import git_config
import wrapper import wrapper
NUM_BATCH_RETRIEVE_REVISIONID = 300 from error import ManifestParseError
NUM_BATCH_RETRIEVE_REVISIONID = 32
def get_gitc_manifest_dir(): def get_gitc_manifest_dir():
return wrapper.Wrapper().get_gitc_manifest_dir() return wrapper.Wrapper().get_gitc_manifest_dir()
@ -54,7 +57,11 @@ def _set_project_revisions(projects):
if gitcmd.Wait(): if gitcmd.Wait():
print('FATAL: Failed to retrieve revisionExpr for %s' % proj) print('FATAL: Failed to retrieve revisionExpr for %s' % proj)
sys.exit(1) sys.exit(1)
proj.revisionExpr = gitcmd.stdout.split('\t')[0] revisionExpr = gitcmd.stdout.split('\t')[0]
if not revisionExpr:
raise ManifestParseError('Invalid SHA-1 revision project %s (%s)' %
(proj.remote.url, proj.revisionExpr))
proj.revisionExpr = revisionExpr
def _manifest_groups(manifest): def _manifest_groups(manifest):
"""Returns the manifest group string that should be synced """Returns the manifest group string that should be synced
@ -81,7 +88,7 @@ def generate_gitc_manifest(gitc_manifest, manifest, paths=None):
print('Generating GITC Manifest by fetching revision SHAs for each ' print('Generating GITC Manifest by fetching revision SHAs for each '
'project.') 'project.')
if paths is None: 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] groups = [x for x in re.split(r'[,\s]+', _manifest_groups(manifest)) if x]
@ -90,7 +97,7 @@ def generate_gitc_manifest(gitc_manifest, manifest, paths=None):
projects = [p for p in projects if p.MatchesGroups(groups)] projects = [p for p in projects if p.MatchesGroups(groups)]
if gitc_manifest is not None: 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): if not proj.MatchesGroups(groups):
continue continue
@ -118,7 +125,7 @@ def generate_gitc_manifest(gitc_manifest, manifest, paths=None):
index += NUM_BATCH_RETRIEVE_REVISIONID index += NUM_BATCH_RETRIEVE_REVISIONID
if gitc_manifest is not None: 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 proj.old_revision and path in paths:
# If we updated a project that has been started, keep the old-revision # If we updated a project that has been started, keep the old-revision
# updated. # updated.
@ -127,7 +134,7 @@ def generate_gitc_manifest(gitc_manifest, manifest, paths=None):
repo_proj.revisionExpr = None repo_proj.revisionExpr = None
# Convert URLs from relative to absolute. # Convert URLs from relative to absolute.
for name, remote in manifest.remotes.iteritems(): for _name, remote in manifest.remotes.items():
remote.fetchUrl = remote.resolvedFetchUrl remote.fetchUrl = remote.resolvedFetchUrl
# Save the manifest. # Save the manifest.

View File

@ -1,6 +1,7 @@
#!/bin/sh #!/bin/sh
# From Gerrit Code Review 2.14.6
# #
# Part of Gerrit Code Review (http://code.google.com/p/gerrit/) # Part of Gerrit Code Review (https://www.gerritcodereview.com/)
# #
# Copyright (C) 2009 The Android Open Source Project # Copyright (C) 2009 The Android Open Source Project
# #
@ -19,7 +20,7 @@
unset GREP_OPTIONS unset GREP_OPTIONS
CHANGE_ID_AFTER="Bug|Issue" CHANGE_ID_AFTER="Bug|Depends-On|Issue|Test|Feature|Fixes|Fixed"
MSG="$1" MSG="$1"
# Check for, and add if missing, a unique Change-Id # Check for, and add if missing, a unique Change-Id
@ -38,6 +39,12 @@ add_ChangeId() {
return return
fi fi
# Do not add Change-Id to temp commits
if echo "$clean_message" | head -1 | grep -q '^\(fixup\|squash\)!'
then
return
fi
if test "false" = "`git config --bool --get gerrit.createChangeId`" if test "false" = "`git config --bool --get gerrit.createChangeId`"
then then
return return
@ -57,6 +64,10 @@ add_ChangeId() {
AWK=/usr/xpg4/bin/awk AWK=/usr/xpg4/bin/awk
fi fi
# Get core.commentChar from git config or use default symbol
commentChar=`git config --get core.commentChar`
commentChar=${commentChar:-#}
# How this works: # How this works:
# - parse the commit message as (textLine+ blankLine*)* # - parse the commit message as (textLine+ blankLine*)*
# - assume textLine+ to be a footer until proven otherwise # - assume textLine+ to be a footer until proven otherwise
@ -75,8 +86,8 @@ add_ChangeId() {
blankLines = 0 blankLines = 0
} }
# Skip lines starting with "#" without any spaces before it. # Skip lines starting with commentChar without any spaces before it.
/^#/ { next } /^'"$commentChar"'/ { next }
# Skip the line starting with the diff command and everything after it, # Skip the line starting with the diff command and everything after it,
# up to the end of the file, assuming it is only patch data. # up to the end of the file, assuming it is only patch data.

View File

@ -1,9 +1,9 @@
#!/bin/sh #!/bin/sh
# #
# An example hook script to verify if you are on battery, in case you # An example hook script to verify if you are on battery, in case you
# are running Linux or OS X. Called by git-gc --auto with no arguments. # are running Windows, Linux or OS X. Called by git-gc --auto with no
# The hook should exit with non-zero status after issuing an appropriate # arguments. The hook should exit with non-zero status after issuing an
# message if it wants to stop the auto repacking. # appropriate message if it wants to stop the auto repacking.
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -19,7 +19,17 @@
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
if test -x /sbin/on_ac_power && /sbin/on_ac_power if uname -s | grep -q "_NT-"
then
if test -x $SYSTEMROOT/System32/Wbem/wmic
then
STATUS=$(wmic path win32_battery get batterystatus /format:list | tr -d '\r\n')
[ "$STATUS" = "BatteryStatus=2" ] && exit 0 || exit 1
fi
exit 0
fi
if test -x /sbin/on_ac_power && (/sbin/on_ac_power;test $? -ne 1)
then then
exit 0 exit 0
elif test "$(cat /sys/class/power_supply/AC/online 2>/dev/null)" = 1 elif test "$(cat /sys/class/power_supply/AC/online 2>/dev/null)" = 1

61
main.py
View File

@ -1,4 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2008 The Android Open Source Project # Copyright (C) 2008 The Android Open Source Project
# #
@ -14,6 +15,12 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""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 from __future__ import print_function
import getpass import getpass
import imp import imp
@ -37,7 +44,8 @@ except ImportError:
kerberos = None kerberos = None
from color import SetDefaultColoring from color import SetDefaultColoring
from trace import SetTrace import event_log
from repo_trace import SetTrace
from git_command import git, GitCommand from git_command import git, GitCommand
from git_config import init_ssh, close_ssh from git_config import init_ssh, close_ssh
from command import InteractiveCommand from command import InteractiveCommand
@ -54,15 +62,13 @@ from error import NoSuchProjectError
from error import RepoChangedException from error import RepoChangedException
import gitc_utils import gitc_utils
from manifest_xml import GitcManifest, XmlManifest from manifest_xml import GitcManifest, XmlManifest
from pager import RunPager from pager import RunPager, TerminatePager
from wrapper import WrapperPath, Wrapper from wrapper import WrapperPath, Wrapper
from subcmds import all_commands from subcmds import all_commands
if not is_python3(): if not is_python3():
# pylint:disable=W0622
input = raw_input input = raw_input
# pylint:enable=W0622
global_options = optparse.OptionParser( global_options = optparse.OptionParser(
usage="repo [-p|--paginate|--no-pager] COMMAND [ARGS]" usage="repo [-p|--paginate|--no-pager] COMMAND [ARGS]"
@ -78,13 +84,19 @@ global_options.add_option('--color',
help='control color usage: auto, always, never') help='control color usage: auto, always, never')
global_options.add_option('--trace', global_options.add_option('--trace',
dest='trace', action='store_true', 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', global_options.add_option('--time',
dest='time', action='store_true', dest='time', action='store_true',
help='time repo command execution') help='time repo command execution')
global_options.add_option('--version', global_options.add_option('--version',
dest='show_version', action='store_true', dest='show_version', action='store_true',
help='display this version of repo') help='display this version of repo')
global_options.add_option('--event-log',
dest='event_log', action='store',
help='filename of event log to append timeline to')
class _Repo(object): class _Repo(object):
def __init__(self, repodir): def __init__(self, repodir):
@ -93,8 +105,8 @@ class _Repo(object):
# add 'branch' as an alias for 'branches' # add 'branch' as an alias for 'branches'
all_commands['branch'] = all_commands['branches'] all_commands['branch'] = all_commands['branches']
def _Run(self, argv): def _ParseArgs(self, argv):
result = 0 """Parse the main `repo` command line options."""
name = None name = None
glob = [] glob = []
@ -111,6 +123,12 @@ class _Repo(object):
argv = [] argv = []
gopts, _gargs = global_options.parse_args(glob) gopts, _gargs = global_options.parse_args(glob)
return (name, gopts, argv)
def _Run(self, name, gopts, argv):
"""Execute the requested subcommand."""
result = 0
if gopts.trace: if gopts.trace:
SetTrace() SetTrace()
if gopts.show_version: if gopts.show_version:
@ -176,7 +194,10 @@ class _Repo(object):
RunPager(config) RunPager(config)
start = time.time() start = time.time()
cmd_event = cmd.event_log.Add(name, event_log.TASK_COMMAND, start)
cmd.event_log.SetParent(cmd_event)
try: try:
cmd.ValidateOptions(copts, cargs)
result = cmd.Execute(copts, cargs) result = cmd.Execute(copts, cargs)
except (DownloadError, ManifestInvalidRevisionError, except (DownloadError, ManifestInvalidRevisionError,
NoManifestException) as e: NoManifestException) as e:
@ -198,8 +219,13 @@ class _Repo(object):
else: else:
print('error: project group must be enabled for the project in the current directory', file=sys.stderr) print('error: project group must be enabled for the project in the current directory', file=sys.stderr)
result = 1 result = 1
except SystemExit as e:
if e.code:
result = e.code
raise
finally: finally:
elapsed = time.time() - start finish = time.time()
elapsed = finish - start
hours, remainder = divmod(elapsed, 3600) hours, remainder = divmod(elapsed, 3600)
minutes, seconds = divmod(remainder, 60) minutes, seconds = divmod(remainder, 60)
if gopts.time: if gopts.time:
@ -209,6 +235,12 @@ class _Repo(object):
print('real\t%dh%dm%.3fs' % (hours, minutes, seconds), print('real\t%dh%dm%.3fs' % (hours, minutes, seconds),
file=sys.stderr) file=sys.stderr)
cmd.event_log.FinishEvent(cmd_event, finish,
result is None or result == 0)
if gopts.event_log:
cmd.event_log.Write(os.path.abspath(
os.path.expanduser(gopts.event_log)))
return result return result
@ -301,7 +333,7 @@ def _UserAgent():
_user_agent = 'git-repo/%s (%s) git/%s Python/%d.%d.%d' % ( _user_agent = 'git-repo/%s (%s) git/%s Python/%d.%d.%d' % (
repo_version, repo_version,
os_name, os_name,
'.'.join(map(str, git.version_tuple())), git.version_tuple().full,
py_version[0], py_version[1], py_version[2]) py_version[0], py_version[1], py_version[2])
return _user_agent return _user_agent
@ -504,7 +536,15 @@ def _Main(argv):
try: try:
init_ssh() init_ssh()
init_http() 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: finally:
close_ssh() close_ssh()
except KeyboardInterrupt: except KeyboardInterrupt:
@ -525,6 +565,7 @@ def _Main(argv):
print('fatal: %s' % e, file=sys.stderr) print('fatal: %s' % e, file=sys.stderr)
result = 128 result = 128
TerminatePager()
sys.exit(result) sys.exit(result)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2008 The Android Open Source Project # Copyright (C) 2008 The Android Open Source Project
# #
@ -32,6 +33,7 @@ else:
import gitc_utils import gitc_utils
from git_config import GitConfig from git_config import GitConfig
from git_refs import R_HEADS, HEAD from git_refs import R_HEADS, HEAD
import platform_utils
from project import RemoteSpec, Project, MetaProject from project import RemoteSpec, Project, MetaProject
from error import ManifestParseError, ManifestInvalidRevisionError from error import ManifestParseError, ManifestInvalidRevisionError
@ -40,18 +42,30 @@ LOCAL_MANIFEST_NAME = 'local_manifest.xml'
LOCAL_MANIFESTS_DIR_NAME = 'local_manifests' LOCAL_MANIFESTS_DIR_NAME = 'local_manifests'
# urljoin gets confused if the scheme is not known. # urljoin gets confused if the scheme is not known.
urllib.parse.uses_relative.extend(['ssh', 'git', 'persistent-https', 'rpc']) urllib.parse.uses_relative.extend([
urllib.parse.uses_netloc.extend(['ssh', 'git', 'persistent-https', 'rpc']) 'ssh',
'git',
'persistent-https',
'sso',
'rpc'])
urllib.parse.uses_netloc.extend([
'ssh',
'git',
'persistent-https',
'sso',
'rpc'])
class _Default(object): class _Default(object):
"""Project defaults within the manifest.""" """Project defaults within the manifest."""
revisionExpr = None revisionExpr = None
destBranchExpr = None destBranchExpr = None
upstreamExpr = None
remote = None remote = None
sync_j = 1 sync_j = 1
sync_c = False sync_c = False
sync_s = False sync_s = False
sync_tags = True
def __eq__(self, other): def __eq__(self, other):
return self.__dict__ == other.__dict__ return self.__dict__ == other.__dict__
@ -64,11 +78,13 @@ class _XmlRemote(object):
name, name,
alias=None, alias=None,
fetch=None, fetch=None,
pushUrl=None,
manifestUrl=None, manifestUrl=None,
review=None, review=None,
revision=None): revision=None):
self.name = name self.name = name
self.fetchUrl = fetch self.fetchUrl = fetch
self.pushUrl = pushUrl
self.manifestUrl = manifestUrl self.manifestUrl = manifestUrl
self.remoteAlias = alias self.remoteAlias = alias
self.reviewUrl = review self.reviewUrl = review
@ -98,11 +114,17 @@ class _XmlRemote(object):
return url return url
def ToRemoteSpec(self, projectName): def ToRemoteSpec(self, projectName):
url = self.resolvedFetchUrl.rstrip('/') + '/' + projectName fetchUrl = self.resolvedFetchUrl.rstrip('/')
url = fetchUrl + '/' + projectName
remoteName = self.name remoteName = self.name
if self.remoteAlias: if self.remoteAlias:
remoteName = self.remoteAlias remoteName = self.remoteAlias
return RemoteSpec(remoteName, url, self.reviewUrl) return RemoteSpec(remoteName,
url=url,
pushUrl=self.pushUrl,
review=self.reviewUrl,
orig_name=self.name,
fetchUrl=self.fetchUrl)
class XmlManifest(object): class XmlManifest(object):
"""manages the repo configuration file""" """manages the repo configuration file"""
@ -114,6 +136,7 @@ class XmlManifest(object):
self.globalConfig = GitConfig.ForUser() self.globalConfig = GitConfig.ForUser()
self.localManifestWarning = False self.localManifestWarning = False
self.isGitcClient = False self.isGitcClient = False
self._load_local_manifests = True
self.repoProject = MetaProject(self, 'repo', self.repoProject = MetaProject(self, 'repo',
gitdir = os.path.join(repodir, 'repo/.git'), gitdir = os.path.join(repodir, 'repo/.git'),
@ -125,15 +148,26 @@ class XmlManifest(object):
self._Unload() self._Unload()
def Override(self, name): def Override(self, name, load_local_manifests=True):
"""Use a different manifest, just for the current instantiation. """Use a different manifest, just for the current instantiation.
""" """
path = os.path.join(self.manifestProject.worktree, name) path = None
if not os.path.isfile(path):
raise ManifestParseError('manifest %s not found' % name) # 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 old = self.manifestFile
try: try:
self._load_local_manifests = load_local_manifests
self.manifestFile = path self.manifestFile = path
self._Unload() self._Unload()
self._Load() self._Load()
@ -147,8 +181,8 @@ class XmlManifest(object):
try: try:
if os.path.lexists(self.manifestFile): if os.path.lexists(self.manifestFile):
os.remove(self.manifestFile) platform_utils.remove(self.manifestFile)
os.symlink('manifests/%s' % name, self.manifestFile) platform_utils.symlink(os.path.join('manifests', name), self.manifestFile)
except OSError as e: except OSError as e:
raise ManifestParseError('cannot link manifest %s: %s' % (name, str(e))) raise ManifestParseError('cannot link manifest %s: %s' % (name, str(e)))
@ -157,6 +191,8 @@ class XmlManifest(object):
root.appendChild(e) root.appendChild(e)
e.setAttribute('name', r.name) e.setAttribute('name', r.name)
e.setAttribute('fetch', r.fetchUrl) e.setAttribute('fetch', r.fetchUrl)
if r.pushUrl is not None:
e.setAttribute('pushurl', r.pushUrl)
if r.remoteAlias is not None: if r.remoteAlias is not None:
e.setAttribute('alias', r.remoteAlias) e.setAttribute('alias', r.remoteAlias)
if r.reviewUrl is not None: if r.reviewUrl is not None:
@ -208,6 +244,9 @@ class XmlManifest(object):
if d.destBranchExpr: if d.destBranchExpr:
have_default = True have_default = True
e.setAttribute('dest-branch', d.destBranchExpr) e.setAttribute('dest-branch', d.destBranchExpr)
if d.upstreamExpr:
have_default = True
e.setAttribute('upstream', d.upstreamExpr)
if d.sync_j > 1: if d.sync_j > 1:
have_default = True have_default = True
e.setAttribute('sync-j', '%d' % d.sync_j) e.setAttribute('sync-j', '%d' % d.sync_j)
@ -217,6 +256,9 @@ class XmlManifest(object):
if d.sync_s: if d.sync_s:
have_default = True have_default = True
e.setAttribute('sync-s', 'true') e.setAttribute('sync-s', 'true')
if not d.sync_tags:
have_default = True
e.setAttribute('sync-tags', 'false')
if have_default: if have_default:
root.appendChild(e) root.appendChild(e)
root.appendChild(doc.createTextNode('')) root.appendChild(doc.createTextNode(''))
@ -249,9 +291,9 @@ class XmlManifest(object):
e.setAttribute('path', relpath) e.setAttribute('path', relpath)
remoteName = None remoteName = None
if d.remote: if d.remote:
remoteName = d.remote.remoteAlias or d.remote.name remoteName = d.remote.name
if not d.remote or p.remote.name != remoteName: if not d.remote or p.remote.orig_name != remoteName:
remoteName = p.remote.name remoteName = p.remote.orig_name
e.setAttribute('remote', remoteName) e.setAttribute('remote', remoteName)
if peg_rev: if peg_rev:
if self.IsMirror: if self.IsMirror:
@ -267,10 +309,11 @@ class XmlManifest(object):
# isn't our value # isn't our value
e.setAttribute('upstream', p.revisionExpr) e.setAttribute('upstream', p.revisionExpr)
else: else:
revision = self.remotes[remoteName].revision or d.revisionExpr revision = self.remotes[p.remote.orig_name].revision or d.revisionExpr
if not revision or revision != p.revisionExpr: if not revision or revision != p.revisionExpr:
e.setAttribute('revision', p.revisionExpr) e.setAttribute('revision', p.revisionExpr)
if p.upstream and p.upstream != p.revisionExpr: if (p.upstream and (p.upstream != p.revisionExpr or
p.upstream != d.upstreamExpr)):
e.setAttribute('upstream', p.upstream) e.setAttribute('upstream', p.upstream)
if p.dest_branch and p.dest_branch != d.destBranchExpr: if p.dest_branch and p.dest_branch != d.destBranchExpr:
@ -306,6 +349,9 @@ class XmlManifest(object):
if p.sync_s: if p.sync_s:
e.setAttribute('sync-s', 'true') e.setAttribute('sync-s', 'true')
if not p.sync_tags:
e.setAttribute('sync-tags', 'false')
if p.clone_depth: if p.clone_depth:
e.setAttribute('clone-depth', str(p.clone_depth)) e.setAttribute('clone-depth', str(p.clone_depth))
@ -367,6 +413,12 @@ class XmlManifest(object):
self._Load() self._Load()
return self._manifest_server 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 @property
def IsMirror(self): def IsMirror(self):
return self.manifestProject.config.GetBoolean('repo.mirror') return self.manifestProject.config.GetBoolean('repo.mirror')
@ -375,6 +427,10 @@ class XmlManifest(object):
def IsArchive(self): def IsArchive(self):
return self.manifestProject.config.GetBoolean('repo.archive') return self.manifestProject.config.GetBoolean('repo.archive')
@property
def HasSubmodules(self):
return self.manifestProject.config.GetBoolean('repo.submodules')
def _Unload(self): def _Unload(self):
self._loaded = False self._loaded = False
self._projects = {} self._projects = {}
@ -398,23 +454,26 @@ class XmlManifest(object):
nodes.append(self._ParseManifestXml(self.manifestFile, nodes.append(self._ParseManifestXml(self.manifestFile,
self.manifestProject.worktree)) self.manifestProject.worktree))
local = os.path.join(self.repodir, LOCAL_MANIFEST_NAME) if self._load_local_manifests:
if os.path.exists(local): local = os.path.join(self.repodir, LOCAL_MANIFEST_NAME)
if not self.localManifestWarning: if os.path.exists(local):
self.localManifestWarning = True if not self.localManifestWarning:
print('warning: %s is deprecated; put local manifests in `%s` instead' self.localManifestWarning = True
% (LOCAL_MANIFEST_NAME, os.path.join(self.repodir, LOCAL_MANIFESTS_DIR_NAME)), print('warning: %s is deprecated; put local manifests '
file=sys.stderr) 'in `%s` instead' % (LOCAL_MANIFEST_NAME,
nodes.append(self._ParseManifestXml(local, self.repodir)) 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)) local_dir = os.path.abspath(os.path.join(self.repodir,
try: LOCAL_MANIFESTS_DIR_NAME))
for local_file in sorted(os.listdir(local_dir)): try:
if local_file.endswith('.xml'): for local_file in sorted(platform_utils.listdir(local_dir)):
local = os.path.join(local_dir, local_file) if local_file.endswith('.xml'):
nodes.append(self._ParseManifestXml(local, self.repodir)) local = os.path.join(local_dir, local_file)
except OSError: nodes.append(self._ParseManifestXml(local, self.repodir))
pass except OSError:
pass
try: try:
self._ParseManifest(nodes) self._ParseManifest(nodes)
@ -446,8 +505,7 @@ class XmlManifest(object):
raise ManifestParseError("no <manifest> in %s" % (path,)) raise ManifestParseError("no <manifest> in %s" % (path,))
nodes = [] nodes = []
for node in manifest.childNodes: # pylint:disable=W0631 for node in manifest.childNodes:
# We only get here if manifest is initialised
if node.nodeName == 'include': if node.nodeName == 'include':
name = self._reqatt(node, 'name') name = self._reqatt(node, 'name')
fp = os.path.join(include_root, name) fp = os.path.join(include_root, name)
@ -462,7 +520,7 @@ class XmlManifest(object):
raise raise
except Exception as e: except Exception as e:
raise ManifestParseError( raise ManifestParseError(
"failed parsing included manifest %s: %s", (name, e)) "failed parsing included manifest %s: %s" % (name, e))
else: else:
nodes.append(node) nodes.append(node)
return nodes return nodes
@ -539,12 +597,15 @@ class XmlManifest(object):
groups = node.getAttribute('groups') groups = node.getAttribute('groups')
if groups: if groups:
groups = self._ParseGroups(groups) groups = self._ParseGroups(groups)
revision = node.getAttribute('revision')
for p in self._projects[name]: for p in self._projects[name]:
if path and p.relpath != path: if path and p.relpath != path:
continue continue
if groups: if groups:
p.groups.extend(groups) p.groups.extend(groups)
if revision:
p.revisionExpr = revision
if node.nodeName == 'repo-hooks': if node.nodeName == 'repo-hooks':
# Get the name of the project and the (space-separated) list of enabled. # Get the name of the project and the (space-separated) list of enabled.
repo_hooks_project = self._reqatt(node, 'in-project') repo_hooks_project = self._reqatt(node, 'in-project')
@ -636,6 +697,9 @@ class XmlManifest(object):
if alias == '': if alias == '':
alias = None alias = None
fetch = self._reqatt(node, 'fetch') fetch = self._reqatt(node, 'fetch')
pushUrl = node.getAttribute('pushurl')
if pushUrl == '':
pushUrl = None
review = node.getAttribute('review') review = node.getAttribute('review')
if review == '': if review == '':
review = None review = None
@ -643,7 +707,7 @@ class XmlManifest(object):
if revision == '': if revision == '':
revision = None revision = None
manifestUrl = self.manifestProject.config.GetString('remote.origin.url') manifestUrl = self.manifestProject.config.GetString('remote.origin.url')
return _XmlRemote(name, alias, fetch, manifestUrl, review, revision) return _XmlRemote(name, alias, fetch, pushUrl, manifestUrl, review, revision)
def _ParseDefault(self, node): def _ParseDefault(self, node):
""" """
@ -656,6 +720,7 @@ class XmlManifest(object):
d.revisionExpr = None d.revisionExpr = None
d.destBranchExpr = node.getAttribute('dest-branch') or None d.destBranchExpr = node.getAttribute('dest-branch') or None
d.upstreamExpr = node.getAttribute('upstream') or None
sync_j = node.getAttribute('sync-j') sync_j = node.getAttribute('sync-j')
if sync_j == '' or sync_j is None: if sync_j == '' or sync_j is None:
@ -674,6 +739,12 @@ class XmlManifest(object):
d.sync_s = False d.sync_s = False
else: else:
d.sync_s = sync_s.lower() in ("yes", "true", "1") d.sync_s = sync_s.lower() in ("yes", "true", "1")
sync_tags = node.getAttribute('sync-tags')
if not sync_tags:
d.sync_tags = True
else:
d.sync_tags = sync_tags.lower() in ("yes", "true", "1")
return d return d
def _ParseNotice(self, node): def _ParseNotice(self, node):
@ -768,6 +839,12 @@ class XmlManifest(object):
else: else:
sync_s = sync_s.lower() in ("yes", "true", "1") sync_s = sync_s.lower() in ("yes", "true", "1")
sync_tags = node.getAttribute('sync-tags')
if not sync_tags:
sync_tags = self._default.sync_tags
else:
sync_tags = sync_tags.lower() in ("yes", "true", "1")
clone_depth = node.getAttribute('clone-depth') clone_depth = node.getAttribute('clone-depth')
if clone_depth: if clone_depth:
try: try:
@ -780,7 +857,7 @@ class XmlManifest(object):
dest_branch = node.getAttribute('dest-branch') or self._default.destBranchExpr dest_branch = node.getAttribute('dest-branch') or self._default.destBranchExpr
upstream = node.getAttribute('upstream') upstream = node.getAttribute('upstream') or self._default.upstreamExpr
groups = '' groups = ''
if node.hasAttribute('groups'): if node.hasAttribute('groups'):
@ -813,6 +890,7 @@ class XmlManifest(object):
groups = groups, groups = groups,
sync_c = sync_c, sync_c = sync_c,
sync_s = sync_s, sync_s = sync_s,
sync_tags = sync_tags,
clone_depth = clone_depth, clone_depth = clone_depth,
upstream = upstream, upstream = upstream,
parent = parent, parent = parent,
@ -969,5 +1047,5 @@ class GitcManifest(XmlManifest):
def _output_manifest_project_extras(self, p, e): def _output_manifest_project_extras(self, p, e):
"""Output GITC Specific Project attributes""" """Output GITC Specific Project attributes"""
if p.old_revision: if p.old_revision:
e.setAttribute('old-revision', str(p.old_revision)) e.setAttribute('old-revision', str(p.old_revision))

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2008 The Android Open Source Project # Copyright (C) 2008 The Android Open Source Project
# #
@ -16,19 +17,53 @@
from __future__ import print_function from __future__ import print_function
import os import os
import select import select
import subprocess
import sys import sys
import platform_utils
active = False active = False
pager_process = None
old_stdout = None
old_stderr = None
def RunPager(globalConfig): def RunPager(globalConfig):
global active
if not os.isatty(0) or not os.isatty(1): if not os.isatty(0) or not os.isatty(1):
return return
pager = _SelectPager(globalConfig) pager = _SelectPager(globalConfig)
if pager == '' or pager == 'cat': if pager == '' or pager == 'cat':
return return
if platform_utils.isWindows():
_PipePager(pager);
else:
_ForkPager(pager)
def TerminatePager():
global pager_process, old_stdout, old_stderr
if pager_process:
sys.stdout.flush()
sys.stderr.flush()
pager_process.stdin.close()
pager_process.wait();
pager_process = None
# Restore initial stdout/err in case there is more output in this process
# after shutting down the pager process
sys.stdout = old_stdout
sys.stderr = old_stderr
def _PipePager(pager):
global pager_process, old_stdout, old_stderr
assert pager_process is None, "Only one active pager process at a time"
# Create pager process, piping stdout/err into its stdin
pager_process = subprocess.Popen([pager], stdin=subprocess.PIPE, stdout=sys.stdout, stderr=sys.stderr)
old_stdout = sys.stdout
old_stderr = sys.stderr
sys.stdout = pager_process.stdin
sys.stderr = pager_process.stdin
def _ForkPager(pager):
global active
# This process turns into the pager; a child it forks will # This process turns into the pager; a child it forks will
# do the real processing and output back to the pager. This # do the real processing and output back to the pager. This
# is necessary to keep the pager in control of the tty. # is necessary to keep the pager in control of the tty.

415
platform_utils.py Normal file
View File

@ -0,0 +1,415 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2016 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.
import errno
import os
import platform
import select
import shutil
import stat
from pyversion import is_python3
if is_python3():
from queue import Queue
else:
from Queue import Queue
from threading import Thread
def isWindows():
""" Returns True when running with the native port of Python for Windows,
False when running on any other platform (including the Cygwin port of
Python).
"""
# Note: The cygwin port of Python returns "CYGWIN_NT_xxx"
return platform.system() == "Windows"
class FileDescriptorStreams(object):
""" Platform agnostic abstraction enabling non-blocking I/O over a
collection of file descriptors. This abstraction is required because
fctnl(os.O_NONBLOCK) is not supported on Windows.
"""
@classmethod
def create(cls):
""" Factory method: instantiates the concrete class according to the
current platform.
"""
if isWindows():
return _FileDescriptorStreamsThreads()
else:
return _FileDescriptorStreamsNonBlocking()
def __init__(self):
self.streams = []
def add(self, fd, dest, std_name):
""" Wraps an existing file descriptor as a stream.
"""
self.streams.append(self._create_stream(fd, dest, std_name))
def remove(self, stream):
""" Removes a stream, when done with it.
"""
self.streams.remove(stream)
@property
def is_done(self):
""" Returns True when all streams have been processed.
"""
return len(self.streams) == 0
def select(self):
""" Returns the set of streams that have data available to read.
The returned streams each expose a read() and a close() method.
When done with a stream, call the remove(stream) method.
"""
raise NotImplementedError
def _create_stream(fd, dest, std_name):
""" Creates a new stream wrapping an existing file descriptor.
"""
raise NotImplementedError
class _FileDescriptorStreamsNonBlocking(FileDescriptorStreams):
""" Implementation of FileDescriptorStreams for platforms that support
non blocking I/O.
"""
class Stream(object):
""" Encapsulates a file descriptor """
def __init__(self, fd, dest, std_name):
self.fd = fd
self.dest = dest
self.std_name = std_name
self.set_non_blocking()
def set_non_blocking(self):
import fcntl
flags = fcntl.fcntl(self.fd, fcntl.F_GETFL)
fcntl.fcntl(self.fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
def fileno(self):
return self.fd.fileno()
def read(self):
return self.fd.read(4096)
def close(self):
self.fd.close()
def _create_stream(self, fd, dest, std_name):
return self.Stream(fd, dest, std_name)
def select(self):
ready_streams, _, _ = select.select(self.streams, [], [])
return ready_streams
class _FileDescriptorStreamsThreads(FileDescriptorStreams):
""" Implementation of FileDescriptorStreams for platforms that don't support
non blocking I/O. This implementation requires creating threads issuing
blocking read operations on file descriptors.
"""
def __init__(self):
super(_FileDescriptorStreamsThreads, self).__init__()
# The queue is shared accross all threads so we can simulate the
# behavior of the select() function
self.queue = Queue(10) # Limit incoming data from streams
def _create_stream(self, fd, dest, std_name):
return self.Stream(fd, dest, std_name, self.queue)
def select(self):
# Return only one stream at a time, as it is the most straighforward
# thing to do and it is compatible with the select() function.
item = self.queue.get()
stream = item.stream
stream.data = item.data
return [stream]
class QueueItem(object):
""" Item put in the shared queue """
def __init__(self, stream, data):
self.stream = stream
self.data = data
class Stream(object):
""" Encapsulates a file descriptor """
def __init__(self, fd, dest, std_name, queue):
self.fd = fd
self.dest = dest
self.std_name = std_name
self.queue = queue
self.data = None
self.thread = Thread(target=self.read_to_queue)
self.thread.daemon = True
self.thread.start()
def close(self):
self.fd.close()
def read(self):
data = self.data
self.data = None
return data
def read_to_queue(self):
""" The thread function: reads everything from the file descriptor into
the shared queue and terminates when reaching EOF.
"""
for line in iter(self.fd.readline, b''):
self.queue.put(_FileDescriptorStreamsThreads.QueueItem(self, line))
self.fd.close()
self.queue.put(_FileDescriptorStreamsThreads.QueueItem(self, None))
def symlink(source, link_name):
"""Creates a symbolic link pointing to source named link_name.
Note: On Windows, source must exist on disk, as the implementation needs
to know whether to create a "File" or a "Directory" symbolic link.
"""
if isWindows():
import platform_utils_win32
source = _validate_winpath(source)
link_name = _validate_winpath(link_name)
target = os.path.join(os.path.dirname(link_name), source)
if isdir(target):
platform_utils_win32.create_dirsymlink(_makelongpath(source), link_name)
else:
platform_utils_win32.create_filesymlink(_makelongpath(source), link_name)
else:
return os.symlink(source, link_name)
def _validate_winpath(path):
path = os.path.normpath(path)
if _winpath_is_valid(path):
return path
raise ValueError("Path \"%s\" must be a relative path or an absolute "
"path starting with a drive letter".format(path))
def _winpath_is_valid(path):
"""Windows only: returns True if path is relative (e.g. ".\\foo") or is
absolute including a drive letter (e.g. "c:\\foo"). Returns False if path
is ambiguous (e.g. "x:foo" or "\\foo").
"""
assert isWindows()
path = os.path.normpath(path)
drive, tail = os.path.splitdrive(path)
if tail:
if not drive:
return tail[0] != os.sep # "\\foo" is invalid
else:
return tail[0] == os.sep # "x:foo" is invalid
else:
return not drive # "x:" is invalid
def _makelongpath(path):
"""Return the input path normalized to support the Windows long path syntax
("\\\\?\\" prefix) if needed, i.e. if the input path is longer than the
MAX_PATH limit.
"""
if isWindows():
# Note: MAX_PATH is 260, but, for directories, the maximum value is actually 246.
if len(path) < 246:
return path
if path.startswith(u"\\\\?\\"):
return path
if not os.path.isabs(path):
return path
# Append prefix and ensure unicode so that the special longpath syntax
# is supported by underlying Win32 API calls
return u"\\\\?\\" + os.path.normpath(path)
else:
return path
def rmtree(path):
"""shutil.rmtree(path) wrapper with support for long paths on Windows.
Availability: Unix, Windows."""
if isWindows():
shutil.rmtree(_makelongpath(path), onerror=handle_rmtree_error)
else:
shutil.rmtree(path)
def handle_rmtree_error(function, path, excinfo):
# Allow deleting read-only files
os.chmod(path, stat.S_IWRITE)
function(path)
def rename(src, dst):
"""os.rename(src, dst) wrapper with support for long paths on Windows.
Availability: Unix, Windows."""
if isWindows():
# On Windows, rename fails if destination exists, see
# https://docs.python.org/2/library/os.html#os.rename
try:
os.rename(_makelongpath(src), _makelongpath(dst))
except OSError as e:
if e.errno == errno.EEXIST:
os.remove(_makelongpath(dst))
os.rename(_makelongpath(src), _makelongpath(dst))
else:
raise
else:
os.rename(src, dst)
def remove(path):
"""Remove (delete) the file path. This is a replacement for os.remove that
allows deleting read-only files on Windows, with support for long paths and
for deleting directory symbolic links.
Availability: Unix, Windows."""
if isWindows():
longpath = _makelongpath(path)
try:
os.remove(longpath)
except OSError as e:
if e.errno == errno.EACCES:
os.chmod(longpath, stat.S_IWRITE)
# Directory symbolic links must be deleted with 'rmdir'.
if islink(longpath) and isdir(longpath):
os.rmdir(longpath)
else:
os.remove(longpath)
else:
raise
else:
os.remove(path)
def walk(top, topdown=True, onerror=None, followlinks=False):
"""os.walk(path) wrapper with support for long paths on Windows.
Availability: Windows, Unix.
"""
if isWindows():
return _walk_windows_impl(top, topdown, onerror, followlinks)
else:
return os.walk(top, topdown, onerror, followlinks)
def _walk_windows_impl(top, topdown, onerror, followlinks):
try:
names = listdir(top)
except Exception as err:
if onerror is not None:
onerror(err)
return
dirs, nondirs = [], []
for name in names:
if isdir(os.path.join(top, name)):
dirs.append(name)
else:
nondirs.append(name)
if topdown:
yield top, dirs, nondirs
for name in dirs:
new_path = os.path.join(top, name)
if followlinks or not islink(new_path):
for x in _walk_windows_impl(new_path, topdown, onerror, followlinks):
yield x
if not topdown:
yield top, dirs, nondirs
def listdir(path):
"""os.listdir(path) wrapper with support for long paths on Windows.
Availability: Windows, Unix.
"""
return os.listdir(_makelongpath(path))
def rmdir(path):
"""os.rmdir(path) wrapper with support for long paths on Windows.
Availability: Windows, Unix.
"""
os.rmdir(_makelongpath(path))
def isdir(path):
"""os.path.isdir(path) wrapper with support for long paths on Windows.
Availability: Windows, Unix.
"""
return os.path.isdir(_makelongpath(path))
def islink(path):
"""os.path.islink(path) wrapper with support for long paths on Windows.
Availability: Windows, Unix.
"""
if isWindows():
import platform_utils_win32
return platform_utils_win32.islink(_makelongpath(path))
else:
return os.path.islink(path)
def readlink(path):
"""Return a string representing the path to which the symbolic link
points. The result may be either an absolute or relative pathname;
if it is relative, it may be converted to an absolute pathname using
os.path.join(os.path.dirname(path), result).
Availability: Windows, Unix.
"""
if isWindows():
import platform_utils_win32
return platform_utils_win32.readlink(_makelongpath(path))
else:
return os.readlink(path)
def realpath(path):
"""Return the canonical path of the specified filename, eliminating
any symbolic links encountered in the path.
Availability: Windows, Unix.
"""
if isWindows():
current_path = os.path.abspath(path)
path_tail = []
for c in range(0, 100): # Avoid cycles
if islink(current_path):
target = readlink(current_path)
current_path = os.path.join(os.path.dirname(current_path), target)
else:
basename = os.path.basename(current_path)
if basename == '':
path_tail.append(current_path)
break
path_tail.append(basename)
current_path = os.path.dirname(current_path)
path_tail.reverse()
result = os.path.normpath(os.path.join(*path_tail))
return result
else:
return os.path.realpath(path)

219
platform_utils_win32.py Normal file
View File

@ -0,0 +1,219 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2016 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.
import errno
from ctypes import WinDLL, get_last_error, FormatError, WinError, addressof
from ctypes import c_buffer
from ctypes.wintypes import BOOL, BOOLEAN, LPCWSTR, DWORD, HANDLE, POINTER, c_ubyte
from ctypes.wintypes import WCHAR, USHORT, LPVOID, Structure, Union, ULONG
from ctypes.wintypes import byref
kernel32 = WinDLL('kernel32', use_last_error=True)
LPDWORD = POINTER(DWORD)
UCHAR = c_ubyte
# Win32 error codes
ERROR_SUCCESS = 0
ERROR_NOT_SUPPORTED = 50
ERROR_PRIVILEGE_NOT_HELD = 1314
# Win32 API entry points
CreateSymbolicLinkW = kernel32.CreateSymbolicLinkW
CreateSymbolicLinkW.restype = BOOLEAN
CreateSymbolicLinkW.argtypes = (LPCWSTR, # lpSymlinkFileName In
LPCWSTR, # lpTargetFileName In
DWORD) # dwFlags In
# Symbolic link creation flags
SYMBOLIC_LINK_FLAG_FILE = 0x00
SYMBOLIC_LINK_FLAG_DIRECTORY = 0x01
# symlink support for CreateSymbolicLink() starting with Windows 10 (1703, v10.0.14972)
SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE = 0x02
GetFileAttributesW = kernel32.GetFileAttributesW
GetFileAttributesW.restype = DWORD
GetFileAttributesW.argtypes = (LPCWSTR,) # lpFileName In
INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF
FILE_ATTRIBUTE_REPARSE_POINT = 0x00400
CreateFileW = kernel32.CreateFileW
CreateFileW.restype = HANDLE
CreateFileW.argtypes = (LPCWSTR, # lpFileName In
DWORD, # dwDesiredAccess In
DWORD, # dwShareMode In
LPVOID, # lpSecurityAttributes In_opt
DWORD, # dwCreationDisposition In
DWORD, # dwFlagsAndAttributes In
HANDLE) # hTemplateFile In_opt
CloseHandle = kernel32.CloseHandle
CloseHandle.restype = BOOL
CloseHandle.argtypes = (HANDLE,) # hObject In
INVALID_HANDLE_VALUE = HANDLE(-1).value
OPEN_EXISTING = 3
FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
DeviceIoControl = kernel32.DeviceIoControl
DeviceIoControl.restype = BOOL
DeviceIoControl.argtypes = (HANDLE, # hDevice In
DWORD, # dwIoControlCode In
LPVOID, # lpInBuffer In_opt
DWORD, # nInBufferSize In
LPVOID, # lpOutBuffer Out_opt
DWORD, # nOutBufferSize In
LPDWORD, # lpBytesReturned Out_opt
LPVOID) # lpOverlapped Inout_opt
# Device I/O control flags and options
FSCTL_GET_REPARSE_POINT = 0x000900A8
IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003
IO_REPARSE_TAG_SYMLINK = 0xA000000C
MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 0x4000
class GENERIC_REPARSE_BUFFER(Structure):
_fields_ = (('DataBuffer', UCHAR * 1),)
class SYMBOLIC_LINK_REPARSE_BUFFER(Structure):
_fields_ = (('SubstituteNameOffset', USHORT),
('SubstituteNameLength', USHORT),
('PrintNameOffset', USHORT),
('PrintNameLength', USHORT),
('Flags', ULONG),
('PathBuffer', WCHAR * 1))
@property
def PrintName(self):
arrayt = WCHAR * (self.PrintNameLength // 2)
offset = type(self).PathBuffer.offset + self.PrintNameOffset
return arrayt.from_address(addressof(self) + offset).value
class MOUNT_POINT_REPARSE_BUFFER(Structure):
_fields_ = (('SubstituteNameOffset', USHORT),
('SubstituteNameLength', USHORT),
('PrintNameOffset', USHORT),
('PrintNameLength', USHORT),
('PathBuffer', WCHAR * 1))
@property
def PrintName(self):
arrayt = WCHAR * (self.PrintNameLength // 2)
offset = type(self).PathBuffer.offset + self.PrintNameOffset
return arrayt.from_address(addressof(self) + offset).value
class REPARSE_DATA_BUFFER(Structure):
class REPARSE_BUFFER(Union):
_fields_ = (('SymbolicLinkReparseBuffer', SYMBOLIC_LINK_REPARSE_BUFFER),
('MountPointReparseBuffer', MOUNT_POINT_REPARSE_BUFFER),
('GenericReparseBuffer', GENERIC_REPARSE_BUFFER))
_fields_ = (('ReparseTag', ULONG),
('ReparseDataLength', USHORT),
('Reserved', USHORT),
('ReparseBuffer', REPARSE_BUFFER))
_anonymous_ = ('ReparseBuffer',)
def create_filesymlink(source, link_name):
"""Creates a Windows file symbolic link source pointing to link_name."""
_create_symlink(source, link_name, SYMBOLIC_LINK_FLAG_FILE)
def create_dirsymlink(source, link_name):
"""Creates a Windows directory symbolic link source pointing to link_name.
"""
_create_symlink(source, link_name, SYMBOLIC_LINK_FLAG_DIRECTORY)
def _create_symlink(source, link_name, dwFlags):
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."
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)
_raise_winerror(
code,
'Error creating symbolic link \"%s\"'.format(link_name))
def islink(path):
result = GetFileAttributesW(path)
if result == INVALID_FILE_ATTRIBUTES:
return False
return bool(result & FILE_ATTRIBUTE_REPARSE_POINT)
def readlink(path):
reparse_point_handle = CreateFileW(path,
0,
0,
None,
OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT |
FILE_FLAG_BACKUP_SEMANTICS,
None)
if reparse_point_handle == INVALID_HANDLE_VALUE:
_raise_winerror(
get_last_error(),
'Error opening symblic link \"%s\"'.format(path))
target_buffer = c_buffer(MAXIMUM_REPARSE_DATA_BUFFER_SIZE)
n_bytes_returned = DWORD()
io_result = DeviceIoControl(reparse_point_handle,
FSCTL_GET_REPARSE_POINT,
None,
0,
target_buffer,
len(target_buffer),
byref(n_bytes_returned),
None)
CloseHandle(reparse_point_handle)
if not io_result:
_raise_winerror(
get_last_error(),
'Error reading symblic 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)
elif rdb.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT:
return _preserve_encoding(path, rdb.MountPointReparseBuffer.PrintName)
# Unsupported reparse point type
_raise_winerror(
ERROR_NOT_SUPPORTED,
'Error reading symblic link \"%s\"'.format(path))
def _preserve_encoding(source, target):
"""Ensures target is the same string type (i.e. unicode or str) as source."""
if isinstance(source, unicode):
return unicode(target)
return str(target)
def _raise_winerror(code, error_desc):
win_error_desc = FormatError(code).strip()
error_desc = "%s: %s".format(error_desc, win_error_desc)
raise WinError(code, error_desc)

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2009 The Android Open Source Project # Copyright (C) 2009 The Android Open Source Project
# #
@ -16,12 +17,18 @@
import os import os
import sys import sys
from time import time from time import time
from trace import IsTrace from repo_trace import IsTrace
_NOT_TTY = not os.isatty(2) _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): class Progress(object):
def __init__(self, title, total=0, units=''): def __init__(self, title, total=0, units='', print_newline=False,
always_print_percentage=False):
self._title = title self._title = title
self._total = total self._total = total
self._done = 0 self._done = 0
@ -29,6 +36,8 @@ class Progress(object):
self._start = time() self._start = time()
self._show = False self._show = False
self._units = units self._units = units
self._print_newline = print_newline
self._always_print_percentage = always_print_percentage
def update(self, inc=1): def update(self, inc=1):
self._done += inc self._done += inc
@ -43,20 +52,23 @@ class Progress(object):
return return
if self._total <= 0: if self._total <= 0:
sys.stderr.write('\r%s: %d, ' % ( sys.stderr.write('%s\r%s: %d,' % (
CSI_ERASE_LINE,
self._title, self._title,
self._done)) self._done))
sys.stderr.flush() sys.stderr.flush()
else: else:
p = (100 * self._done) / self._total p = (100 * self._done) / self._total
if self._lastp != p: if self._lastp != p or self._always_print_percentage:
self._lastp = p self._lastp = p
sys.stderr.write('\r%s: %3d%% (%d%s/%d%s) ' % ( sys.stderr.write('%s\r%s: %3d%% (%d%s/%d%s)%s' % (
CSI_ERASE_LINE,
self._title, self._title,
p, p,
self._done, self._units, self._done, self._units,
self._total, self._units)) self._total, self._units,
"\n" if self._print_newline else ""))
sys.stderr.flush() sys.stderr.flush()
def end(self): def end(self):
@ -64,13 +76,15 @@ class Progress(object):
return return
if self._total <= 0: 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._title,
self._done)) self._done))
sys.stderr.flush() sys.stderr.flush()
else: else:
p = (100 * self._done) / self._total p = (100 * self._done) / self._total
sys.stderr.write('\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, self._title,
p, p,
self._done, self._units, self._done, self._units,

947
project.py Normal file → Executable file

File diff suppressed because it is too large Load Diff

View File

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

275
repo
View File

@ -1,4 +1,14 @@
#!/usr/bin/env python #!/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
# repo default configuration # repo default configuration
# #
@ -23,10 +33,13 @@ REPO_REV = 'stable'
# limitations under the License. # limitations under the License.
# increment this whenever we make important changes to this script # increment this whenever we make important changes to this script
VERSION = (1, 22) VERSION = (1, 25)
# increment this if the MAINTAINER_KEYS block is modified # increment this if the MAINTAINER_KEYS block is modified
KEYRING_VERSION = (1, 2) KEYRING_VERSION = (1, 2)
# Each individual key entry is created by using:
# gpg --armor --export keyid
MAINTAINER_KEYS = """ MAINTAINER_KEYS = """
Repo Maintainer <repo@android.kernel.org> Repo Maintainer <repo@android.kernel.org>
@ -110,13 +123,15 @@ repodir = '.repo' # name of repo's private directory
S_repo = 'repo' # special repo repository S_repo = 'repo' # special repo repository
S_manifests = 'manifests' # special manifest repository S_manifests = 'manifests' # special manifest repository
REPO_MAIN = S_repo + '/main.py' # main script REPO_MAIN = S_repo + '/main.py' # main script
MIN_PYTHON_VERSION = (2, 6) # minimum supported python version MIN_PYTHON_VERSION = (2, 7) # minimum supported python version
GITC_CONFIG_FILE = '/gitc/.config' GITC_CONFIG_FILE = '/gitc/.config'
GITC_FS_ROOT_DIR = '/gitc/manifest-rw/' GITC_FS_ROOT_DIR = '/gitc/manifest-rw/'
import collections
import errno import errno
import optparse import optparse
import platform
import re import re
import shutil import shutil
import stat import stat
@ -134,19 +149,15 @@ else:
urllib.error = urllib2 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)
# Python version check # Python version check
ver = sys.version_info ver = sys.version_info
if (ver[0], ver[1]) < MIN_PYTHON_VERSION: if (ver[0], ver[1]) < MIN_PYTHON_VERSION:
_print('error: Python version %s unsupported.\n' print('error: Python version {} unsupported.\n'
'Please use Python 2.6 - 2.7 instead.' 'Please use Python {}.{} instead.'.format(
% sys.version.split(' ')[0], file=sys.stderr) sys.version.split(' ')[0],
MIN_PYTHON_VERSION[0],
MIN_PYTHON_VERSION[1],
), file=sys.stderr)
sys.exit(1) sys.exit(1)
home_dot_repo = os.path.expanduser('~/.repoconfig') home_dot_repo = os.path.expanduser('~/.repoconfig')
@ -172,6 +183,9 @@ group.add_option('-b', '--manifest-branch',
group.add_option('-m', '--manifest-name', group.add_option('-m', '--manifest-name',
dest='manifest_name', dest='manifest_name',
help='initial manifest file', metavar='NAME.xml') help='initial manifest file', metavar='NAME.xml')
group.add_option('--current-branch',
dest='current_branch_only', action='store_true',
help='fetch only current manifest branch from server')
group.add_option('--mirror', group.add_option('--mirror',
dest='mirror', action='store_true', dest='mirror', action='store_true',
help='create a replica of the remote repositories ' help='create a replica of the remote repositories '
@ -179,13 +193,26 @@ group.add_option('--mirror',
group.add_option('--reference', group.add_option('--reference',
dest='reference', dest='reference',
help='location of mirror directory', metavar='DIR') help='location of mirror directory', metavar='DIR')
group.add_option('--dissociate',
dest='dissociate', action='store_true',
help='dissociate from reference mirrors after clone')
group.add_option('--depth', type='int', default=None, group.add_option('--depth', type='int', default=None,
dest='depth', dest='depth',
help='create a shallow clone with given depth; see git clone') 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', group.add_option('--archive',
dest='archive', action='store_true', dest='archive', action='store_true',
help='checkout an archive instead of a git repository for ' help='checkout an archive instead of a git repository for '
'each project. See git archive.') 'each project. See git archive.')
group.add_option('--submodules',
dest='submodules', action='store_true',
help='sync any submodules associated with the manifest repo')
group.add_option('-g', '--groups', group.add_option('-g', '--groups',
dest='groups', default='default', dest='groups', default='default',
help='restrict manifest projects to ones with specified ' help='restrict manifest projects to ones with specified '
@ -196,6 +223,12 @@ group.add_option('-p', '--platform',
help='restrict manifest projects to ones with a specified ' help='restrict manifest projects to ones with a specified '
'platform group [auto|all|none|linux|darwin|...]', 'platform group [auto|all|none|linux|darwin|...]',
metavar='PLATFORM') metavar='PLATFORM')
group.add_option('--no-clone-bundle',
dest='no_clone_bundle', action='store_true',
help='disable use of /clone.bundle on HTTP/HTTPS')
group.add_option('--no-tags',
dest='no_tags', action='store_true',
help="don't fetch tags in the manifest")
# Tool # Tool
@ -297,21 +330,21 @@ def _Init(args, gitc_init=False):
if branch.startswith('refs/heads/'): if branch.startswith('refs/heads/'):
branch = branch[len('refs/heads/'):] branch = branch[len('refs/heads/'):]
if branch.startswith('refs/'): 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() raise CloneFailure()
try: try:
if gitc_init: if gitc_init:
gitc_manifest_dir = get_gitc_manifest_dir() gitc_manifest_dir = get_gitc_manifest_dir()
if not gitc_manifest_dir: if not gitc_manifest_dir:
_print('fatal: GITC filesystem is not available. Exiting...', print('fatal: GITC filesystem is not available. Exiting...',
file=sys.stderr) file=sys.stderr)
sys.exit(1) sys.exit(1)
gitc_client = opt.gitc_client gitc_client = opt.gitc_client
if not gitc_client: if not gitc_client:
gitc_client = gitc_parse_clientdir(os.getcwd()) gitc_client = gitc_parse_clientdir(os.getcwd())
if not gitc_client: 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) sys.exit(1)
client_dir = os.path.join(gitc_manifest_dir, gitc_client) client_dir = os.path.join(gitc_manifest_dir, gitc_client)
if not os.path.exists(client_dir): if not os.path.exists(client_dir):
@ -324,8 +357,8 @@ def _Init(args, gitc_init=False):
os.mkdir(repodir) os.mkdir(repodir)
except OSError as e: except OSError as e:
if e.errno != errno.EEXIST: if e.errno != errno.EEXIST:
_print('fatal: cannot make %s directory: %s' print('fatal: cannot make %s directory: %s'
% (repodir, e.strerror), file=sys.stderr) % (repodir, e.strerror), file=sys.stderr)
# Don't raise CloneFailure; that would delete the # Don't raise CloneFailure; that would delete the
# name. Instead exit immediately. # name. Instead exit immediately.
# #
@ -339,7 +372,7 @@ def _Init(args, gitc_init=False):
can_verify = True can_verify = True
dst = os.path.abspath(os.path.join(repodir, S_repo)) dst = os.path.abspath(os.path.join(repodir, S_repo))
_Clone(url, dst, opt.quiet) _Clone(url, dst, opt.quiet, not opt.no_clone_bundle)
if can_verify and not opt.no_repo_verify: if can_verify and not opt.no_repo_verify:
rev = _Verify(dst, branch, opt.quiet) rev = _Verify(dst, branch, opt.quiet)
@ -347,52 +380,75 @@ def _Init(args, gitc_init=False):
rev = 'refs/remotes/origin/%s^0' % branch rev = 'refs/remotes/origin/%s^0' % branch
_Checkout(dst, branch, rev, opt.quiet) _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)
except CloneFailure: except CloneFailure:
if opt.quiet: if opt.quiet:
_print('fatal: repo init failed; run without --quiet to see why', print('fatal: repo init failed; run without --quiet to see why',
file=sys.stderr) file=sys.stderr)
raise 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 '): if not ver_str.startswith('git version '):
return None 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 = [] to_tuple = []
for num_str in num_ver_str.split('.')[:3]: for num_str in num_ver_str.split('.')[:3]:
if num_str.isdigit(): if num_str.isdigit():
to_tuple.append(int(num_str)) to_tuple.append(int(num_str))
else: else:
to_tuple.append(0) to_tuple.append(0)
return tuple(to_tuple) to_tuple.append(full_version)
return GitVersion(*to_tuple)
def _CheckGitVersion(): def _GetGitVersion():
cmd = [GIT, '--version'] cmd = [GIT, '--version']
try: try:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
except OSError as e: except OSError as e:
_print(file=sys.stderr) print(file=sys.stderr)
_print("fatal: '%s' is not available" % GIT, file=sys.stderr) print("fatal: '%s' is not available" % GIT, file=sys.stderr)
_print('fatal: %s' % e, file=sys.stderr) print('fatal: %s' % e, file=sys.stderr)
_print(file=sys.stderr) print(file=sys.stderr)
_print('Please make sure %s is installed and in your path.' % GIT, print('Please make sure %s is installed and in your path.' % GIT,
file=sys.stderr) file=sys.stderr)
raise CloneFailure() raise
ver_str = proc.stdout.read().strip() ver_str = proc.stdout.read().strip()
proc.stdout.close() proc.stdout.close()
proc.wait() 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: 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() raise CloneFailure()
if ver_act < MIN_GIT_VERSION: if ver_act < MIN_GIT_VERSION:
need = '.'.join(map(str, 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() raise CloneFailure()
@ -419,20 +475,23 @@ def SetupGnuPG(quiet):
os.mkdir(home_dot_repo) os.mkdir(home_dot_repo)
except OSError as e: except OSError as e:
if e.errno != errno.EEXIST: if e.errno != errno.EEXIST:
_print('fatal: cannot make %s directory: %s' print('fatal: cannot make %s directory: %s'
% (home_dot_repo, e.strerror), file=sys.stderr) % (home_dot_repo, e.strerror), file=sys.stderr)
sys.exit(1) sys.exit(1)
try: try:
os.mkdir(gpg_dir, stat.S_IRWXU) os.mkdir(gpg_dir, stat.S_IRWXU)
except OSError as e: except OSError as e:
if e.errno != errno.EEXIST: if e.errno != errno.EEXIST:
_print('fatal: cannot make %s directory: %s' % (gpg_dir, e.strerror), print('fatal: cannot make %s directory: %s' % (gpg_dir, e.strerror),
file=sys.stderr) file=sys.stderr)
sys.exit(1) sys.exit(1)
env = os.environ.copy() env = os.environ.copy()
env['GNUPGHOME'] = gpg_dir.encode() try:
env['GNUPGHOME'] = gpg_dir
except UnicodeEncodeError:
env['GNUPGHOME'] = gpg_dir.encode()
cmd = ['gpg', '--import'] cmd = ['gpg', '--import']
try: try:
@ -441,18 +500,18 @@ def SetupGnuPG(quiet):
stdin=subprocess.PIPE) stdin=subprocess.PIPE)
except OSError as e: except OSError as e:
if not quiet: if not quiet:
_print('warning: gpg (GnuPG) is not available.', file=sys.stderr) print('warning: gpg (GnuPG) is not available.', file=sys.stderr)
_print('warning: Installing it is strongly encouraged.', file=sys.stderr) print('warning: Installing it is strongly encouraged.', file=sys.stderr)
_print(file=sys.stderr) print(file=sys.stderr)
return False return False
proc.stdin.write(MAINTAINER_KEYS) proc.stdin.write(MAINTAINER_KEYS.encode('utf-8'))
proc.stdin.close() proc.stdin.close()
if proc.wait() != 0: 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) sys.exit(1)
_print() print()
fd = open(os.path.join(home_dot_repo, 'keyring-version'), 'w') fd = open(os.path.join(home_dot_repo, 'keyring-version'), 'w')
fd.write('.'.join(map(str, KEYRING_VERSION)) + '\n') fd.write('.'.join(map(str, KEYRING_VERSION)) + '\n')
@ -479,7 +538,7 @@ def _InitHttp():
p = n.hosts[host] p = n.hosts[host]
mgr.add_password(p[1], 'http://%s/' % host, p[0], p[2]) mgr.add_password(p[1], 'http://%s/' % host, p[0], p[2])
mgr.add_password(p[1], 'https://%s/' % host, p[0], p[2]) mgr.add_password(p[1], 'https://%s/' % host, p[0], p[2])
except: # pylint: disable=bare-except except:
pass pass
handlers.append(urllib.request.HTTPBasicAuthHandler(mgr)) handlers.append(urllib.request.HTTPBasicAuthHandler(mgr))
handlers.append(urllib.request.HTTPDigestAuthHandler(mgr)) handlers.append(urllib.request.HTTPDigestAuthHandler(mgr))
@ -495,7 +554,7 @@ def _InitHttp():
def _Fetch(url, local, src, quiet): def _Fetch(url, local, src, quiet):
if not quiet: if not quiet:
_print('Get %s' % url, file=sys.stderr) print('Get %s' % url, file=sys.stderr)
cmd = [GIT, 'fetch'] cmd = [GIT, 'fetch']
if quiet: if quiet:
@ -505,7 +564,7 @@ def _Fetch(url, local, src, quiet):
err = None err = None
cmd.append(src) cmd.append(src)
cmd.append('+refs/heads/*:refs/remotes/origin/*') cmd.append('+refs/heads/*:refs/remotes/origin/*')
cmd.append('refs/tags/*:refs/tags/*') cmd.append('+refs/tags/*:refs/tags/*')
proc = subprocess.Popen(cmd, cwd=local, stderr=err) proc = subprocess.Popen(cmd, cwd=local, stderr=err)
if err: if err:
@ -525,6 +584,7 @@ def _DownloadBundle(url, local, quiet):
cwd=local, cwd=local,
stdout=subprocess.PIPE) stdout=subprocess.PIPE)
for line in proc.stdout: for line in proc.stdout:
line = line.decode('utf-8')
m = re.compile(r'^url\.(.*)\.insteadof (.*)$').match(line) m = re.compile(r'^url\.(.*)\.insteadof (.*)$').match(line)
if m: if m:
new_url = m.group(1) new_url = m.group(1)
@ -543,21 +603,21 @@ def _DownloadBundle(url, local, quiet):
try: try:
r = urllib.request.urlopen(url) r = urllib.request.urlopen(url)
except urllib.error.HTTPError as e: except urllib.error.HTTPError as e:
if e.code in [401, 403, 404]: if e.code in [401, 403, 404, 501]:
return False return False
_print('fatal: Cannot get %s' % url, file=sys.stderr) print('fatal: Cannot get %s' % url, file=sys.stderr)
_print('fatal: HTTP error %s' % e.code, file=sys.stderr) print('fatal: HTTP error %s' % e.code, file=sys.stderr)
raise CloneFailure() raise CloneFailure()
except urllib.error.URLError as e: except urllib.error.URLError as e:
_print('fatal: Cannot get %s' % url, file=sys.stderr) print('fatal: Cannot get %s' % url, file=sys.stderr)
_print('fatal: error %s' % e.reason, file=sys.stderr) print('fatal: error %s' % e.reason, file=sys.stderr)
raise CloneFailure() raise CloneFailure()
try: try:
if not quiet: if not quiet:
_print('Get %s' % url, file=sys.stderr) print('Get %s' % url, file=sys.stderr)
while True: while True:
buf = r.read(8192) buf = r.read(8192)
if buf == '': if not buf:
return True return True
dest.write(buf) dest.write(buf)
finally: finally:
@ -574,29 +634,29 @@ def _ImportBundle(local):
os.remove(path) os.remove(path)
def _Clone(url, local, quiet): def _Clone(url, local, quiet, clone_bundle):
"""Clones a git repository to a new subdirectory of repodir """Clones a git repository to a new subdirectory of repodir
""" """
try: try:
os.mkdir(local) os.mkdir(local)
except OSError as e: except OSError as e:
_print('fatal: cannot make %s directory: %s' % (local, e.strerror), print('fatal: cannot make %s directory: %s' % (local, e.strerror),
file=sys.stderr) file=sys.stderr)
raise CloneFailure() raise CloneFailure()
cmd = [GIT, 'init', '--quiet'] cmd = [GIT, 'init', '--quiet']
try: try:
proc = subprocess.Popen(cmd, cwd=local) proc = subprocess.Popen(cmd, cwd=local)
except OSError as e: except OSError as e:
_print(file=sys.stderr) print(file=sys.stderr)
_print("fatal: '%s' is not available" % GIT, file=sys.stderr) print("fatal: '%s' is not available" % GIT, file=sys.stderr)
_print('fatal: %s' % e, file=sys.stderr) print('fatal: %s' % e, file=sys.stderr)
_print(file=sys.stderr) print(file=sys.stderr)
_print('Please make sure %s is installed and in your path.' % GIT, print('Please make sure %s is installed and in your path.' % GIT,
file=sys.stderr) file=sys.stderr)
raise CloneFailure() raise CloneFailure()
if proc.wait() != 0: 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() raise CloneFailure()
_InitHttp() _InitHttp()
@ -604,7 +664,7 @@ def _Clone(url, local, quiet):
_SetConfig(local, _SetConfig(local,
'remote.origin.fetch', 'remote.origin.fetch',
'+refs/heads/*:refs/remotes/origin/*') '+refs/heads/*:refs/remotes/origin/*')
if _DownloadBundle(url, local, quiet): if clone_bundle and _DownloadBundle(url, local, quiet):
_ImportBundle(local) _ImportBundle(local)
_Fetch(url, local, 'origin', quiet) _Fetch(url, local, 'origin', quiet)
@ -617,28 +677,31 @@ def _Verify(cwd, branch, quiet):
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
cwd=cwd) cwd=cwd)
cur = proc.stdout.read().strip() cur = proc.stdout.read().strip().decode('utf-8')
proc.stdout.close() proc.stdout.close()
proc.stderr.read() proc.stderr.read()
proc.stderr.close() proc.stderr.close()
if proc.wait() != 0 or not cur: if proc.wait() != 0 or not cur:
_print(file=sys.stderr) print(file=sys.stderr)
_print("fatal: branch '%s' has not been signed" % branch, file=sys.stderr) print("fatal: branch '%s' has not been signed" % branch, file=sys.stderr)
raise CloneFailure() raise CloneFailure()
m = re.compile(r'^(.*)-[0-9]{1,}-g[0-9a-f]{1,}$').match(cur) m = re.compile(r'^(.*)-[0-9]{1,}-g[0-9a-f]{1,}$').match(cur)
if m: if m:
cur = m.group(1) cur = m.group(1)
if not quiet: if not quiet:
_print(file=sys.stderr) print(file=sys.stderr)
_print("info: Ignoring branch '%s'; using tagged release '%s'" print("info: Ignoring branch '%s'; using tagged release '%s'"
% (branch, cur), file=sys.stderr) % (branch, cur), file=sys.stderr)
_print(file=sys.stderr) print(file=sys.stderr)
env = os.environ.copy() env = os.environ.copy()
env['GNUPGHOME'] = gpg_dir.encode() try:
env['GNUPGHOME'] = gpg_dir
except UnicodeEncodeError:
env['GNUPGHOME'] = gpg_dir.encode()
cmd = [GIT, 'tag', '-v', cur] cmd = [GIT, 'tag', '-v', cur]
proc = subprocess.Popen(cmd, proc = subprocess.Popen(cmd,
@ -646,17 +709,17 @@ def _Verify(cwd, branch, quiet):
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
cwd=cwd, cwd=cwd,
env=env) env=env)
out = proc.stdout.read() out = proc.stdout.read().decode('utf-8')
proc.stdout.close() proc.stdout.close()
err = proc.stderr.read() err = proc.stderr.read().decode('utf-8')
proc.stderr.close() proc.stderr.close()
if proc.wait() != 0: if proc.wait() != 0:
_print(file=sys.stderr) print(file=sys.stderr)
_print(out, file=sys.stderr) print(out, file=sys.stderr)
_print(err, file=sys.stderr) print(err, file=sys.stderr)
_print(file=sys.stderr) print(file=sys.stderr)
raise CloneFailure() raise CloneFailure()
return '%s^0' % cur return '%s^0' % cur
@ -727,7 +790,7 @@ def _Usage():
if get_gitc_manifest_dir(): if get_gitc_manifest_dir():
gitc_usage = " gitc-init Initialize a GITC Client.\n" gitc_usage = " gitc-init Initialize a GITC Client.\n"
_print( print(
"""usage: repo COMMAND [ARGS] """usage: repo COMMAND [ARGS]
repo is not yet installed. Use "repo init" to install it here. repo is not yet installed. Use "repo init" to install it here.
@ -739,8 +802,8 @@ The most commonly used repo commands are:
""" help Display detailed help on a command """ help Display detailed help on a command
For access to the full online help, install repo ("repo init"). For access to the full online help, install repo ("repo init").
""", file=sys.stderr) """)
sys.exit(1) sys.exit(0)
def _Help(args): def _Help(args):
@ -753,23 +816,23 @@ def _Help(args):
init_optparse.print_help() init_optparse.print_help()
sys.exit(0) sys.exit(0)
else: else:
_print("error: '%s' is not a bootstrap command.\n" print("error: '%s' is not a bootstrap command.\n"
' For access to online help, install repo ("repo init").' ' For access to online help, install repo ("repo init").'
% args[0], file=sys.stderr) % args[0], file=sys.stderr)
else: else:
_Usage() _Usage()
sys.exit(1) sys.exit(1)
def _NotInstalled(): def _NotInstalled():
_print('error: repo is not installed. Use "repo init" to install it here.', print('error: repo is not installed. Use "repo init" to install it here.',
file=sys.stderr) file=sys.stderr)
sys.exit(1) sys.exit(1)
def _NoCommands(cmd): def _NoCommands(cmd):
_print("""error: command '%s' requires repo to be installed first. print("""error: command '%s' requires repo to be installed first.
Use "repo init" to install it here.""" % cmd, file=sys.stderr) Use "repo init" to install it here.""" % cmd, file=sys.stderr)
sys.exit(1) sys.exit(1)
@ -799,14 +862,14 @@ def _SetDefaultsTo(gitdir):
'HEAD'], 'HEAD'],
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE) stderr=subprocess.PIPE)
REPO_REV = proc.stdout.read().strip() REPO_REV = proc.stdout.read().strip().decode('utf-8')
proc.stdout.close() proc.stdout.close()
proc.stderr.read() proc.stderr.read()
proc.stderr.close() proc.stderr.close()
if proc.wait() != 0: 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) sys.exit(1)
@ -823,10 +886,10 @@ def main(orig_args):
cwd = os.getcwd() cwd = os.getcwd()
if get_gitc_manifest_dir() and cwd.startswith(get_gitc_manifest_dir()): if get_gitc_manifest_dir() and cwd.startswith(get_gitc_manifest_dir()):
_print('error: repo cannot be used in the GITC local manifest directory.' print('error: repo cannot be used in the GITC local manifest directory.'
'\nIf you want to work on this GITC client please rerun this ' '\nIf you want to work on this GITC client please rerun this '
'command from the corresponding client under /gitc/', 'command from the corresponding client under /gitc/',
file=sys.stderr) file=sys.stderr)
sys.exit(1) sys.exit(1)
if not repo_main: if not repo_main:
if opt.help: if opt.help:
@ -841,7 +904,10 @@ def main(orig_args):
try: try:
_Init(args, gitc_init=(cmd == 'gitc-init')) _Init(args, gitc_init=(cmd == 'gitc-init'))
except CloneFailure: except CloneFailure:
shutil.rmtree(os.path.join(repodir, S_repo), ignore_errors=True) path = os.path.join(repodir, S_repo)
print("fatal: cloning the git-repo repository failed, will remove "
"'%s' " % path, file=sys.stderr)
shutil.rmtree(path, ignore_errors=True)
sys.exit(1) sys.exit(1)
repo_main, rel_repo_dir = _FindRepo() repo_main, rel_repo_dir = _FindRepo()
else: else:
@ -859,16 +925,15 @@ def main(orig_args):
me.extend(orig_args) me.extend(orig_args)
me.extend(extra_args) me.extend(extra_args)
try: try:
os.execv(sys.executable, me) if platform.system() == "Windows":
sys.exit(subprocess.call(me))
else:
os.execv(sys.executable, me)
except OSError as e: except OSError as e:
_print("fatal: unable to start %s" % repo_main, file=sys.stderr) print("fatal: unable to start %s" % repo_main, file=sys.stderr)
_print("fatal: %s" % e, file=sys.stderr) print("fatal: %s" % e, file=sys.stderr)
sys.exit(148) sys.exit(148)
if __name__ == '__main__': 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:]) main(sys.argv[1:])

View File

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

54
run_tests Executable file
View File

@ -0,0 +1,54 @@
#!/usr/bin/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:
subprocess.check_call([cmd] + argv)
return 0
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 1
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:]))

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2008 The Android Open Source Project # 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 # Copyright (C) 2008 The Android Open Source Project
# #
@ -16,6 +17,7 @@
from __future__ import print_function from __future__ import print_function
import sys import sys
from command import Command from command import Command
from collections import defaultdict
from git_command import git from git_command import git
from progress import Progress from progress import Progress
@ -23,49 +25,75 @@ class Abandon(Command):
common = True common = True
helpSummary = "Permanently abandon a development branch" helpSummary = "Permanently abandon a development branch"
helpUsage = """ helpUsage = """
%prog <branchname> [<project>...] %prog [--all | <branchname>] [<project>...]
This subcommand permanently abandons a development branch by This subcommand permanently abandons a development branch by
deleting it (and all its history) from your local repository. deleting it (and all its history) from your local repository.
It is equivalent to "git branch -D <branchname>". It is equivalent to "git branch -D <branchname>".
""" """
def _Options(self, p):
p.add_option('--all',
dest='all', action='store_true',
help='delete all branches in all projects')
def Execute(self, opt, args): def ValidateOptions(self, opt, args):
if not args: if not opt.all and not args:
self.Usage() self.Usage()
nb = args[0] if not opt.all:
if not git.check_ref_format('heads/%s' % nb): nb = args[0]
print("error: '%s' is not a valid name" % nb, file=sys.stderr) if not git.check_ref_format('heads/%s' % nb):
sys.exit(1) self.OptionParser.error("'%s' is not a valid branch name" % nb)
else:
args.insert(0, "'All local branches'")
def Execute(self, opt, args):
nb = args[0] nb = args[0]
err = [] err = defaultdict(list)
success = [] success = defaultdict(list)
all_projects = self.GetProjects(args[1:]) all_projects = self.GetProjects(args[1:])
pm = Progress('Abandon %s' % nb, len(all_projects)) pm = Progress('Abandon %s' % nb, len(all_projects))
for project in all_projects: for project in all_projects:
pm.update() pm.update()
status = project.AbandonBranch(nb) if opt.all:
if status is not None: branches = list(project.GetBranches().keys())
if status: else:
success.append(project) branches = [nb]
else:
err.append(project) for name in branches:
status = project.AbandonBranch(name)
if status is not None:
if status:
success[name].append(project)
else:
err[name].append(project)
pm.end() pm.end()
width = 25
for name in branches:
if width < len(name):
width = len(name)
if err: if err:
for p in err: for br in err.keys():
print("error: %s/: cannot abandon %s" % (p.relpath, nb), err_msg = "error: cannot abandon %s" %br
file=sys.stderr) print(err_msg, file=sys.stderr)
for proj in err[br]:
print(' '*len(err_msg) + " | %s" % proj.relpath, file=sys.stderr)
sys.exit(1) sys.exit(1)
elif not success: elif not success:
print('error: no project has branch %s' % nb, file=sys.stderr) print('error: no project has local branch(es) : %s' % nb,
file=sys.stderr)
sys.exit(1) sys.exit(1)
else: else:
print('Abandoned in %d project(s):\n %s' print('Abandoned branches:', file=sys.stderr)
% (len(success), '\n '.join(p.relpath for p in success)), for br in success.keys():
file=sys.stderr) if len(all_projects) > 1 and len(all_projects) == len(success[br]):
result = "all project"
else:
result = "%s" % (
('\n'+' '*width + '| ').join(p.relpath for p in success[br]))
print("%s%s| %s\n" % (br,' '*(width-len(br)), result),file=sys.stderr)

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2009 The Android Open Source Project # Copyright (C) 2009 The Android Open Source Project
# #
@ -67,8 +68,7 @@ class Branches(Command):
Summarizes the currently available topic branches. Summarizes the currently available topic branches.
Branch Display # Branch Display
--------------
The branch display output by this command is organized into four The branch display output by this command is organized into four
columns of information; for example: columns of information; for example:

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2014 The Android Open Source Project # Copyright (C) 2014 The Android Open Source Project
# #
@ -71,6 +72,10 @@ synced and their revisions won't be found.
p.add_option('--no-color', p.add_option('--no-color',
dest='color', action='store_false', default=True, dest='color', action='store_false', default=True,
help='does not display the diff in color.') help='does not display the diff in color.')
p.add_option('--pretty-format',
dest='pretty_format', action='store',
metavar='<FORMAT>',
help='print the log using a custom git pretty format string')
def _printRawDiff(self, diff): def _printRawDiff(self, diff):
for project in diff['added']: for project in diff['added']:
@ -92,7 +97,7 @@ synced and their revisions won't be found.
otherProject.revisionExpr)) otherProject.revisionExpr))
self.out.nl() self.out.nl()
def _printDiff(self, diff, color=True): def _printDiff(self, diff, color=True, pretty_format=None):
if diff['added']: if diff['added']:
self.out.nl() self.out.nl()
self.printText('added projects : \n') self.printText('added projects : \n')
@ -124,7 +129,8 @@ synced and their revisions won't be found.
self.printText(' to ') self.printText(' to ')
self.printRevision(otherProject.revisionExpr) self.printRevision(otherProject.revisionExpr)
self.out.nl() self.out.nl()
self._printLogs(project, otherProject, raw=False, color=color) self._printLogs(project, otherProject, raw=False, color=color,
pretty_format=pretty_format)
self.out.nl() self.out.nl()
if diff['unreachable']: if diff['unreachable']:
@ -139,9 +145,13 @@ synced and their revisions won't be found.
self.printText(' not found') self.printText(' not found')
self.out.nl() self.out.nl()
def _printLogs(self, project, otherProject, raw=False, color=True): def _printLogs(self, project, otherProject, raw=False, color=True,
logs = project.getAddedAndRemovedLogs(otherProject, oneline=True, pretty_format=None):
color=color)
logs = project.getAddedAndRemovedLogs(otherProject,
oneline=(pretty_format is None),
color=color,
pretty_format=pretty_format)
if logs['removed']: if logs['removed']:
removedLogs = logs['removed'].split('\n') removedLogs = logs['removed'].split('\n')
for log in removedLogs: for log in removedLogs:
@ -166,10 +176,11 @@ synced and their revisions won't be found.
self.printText(log) self.printText(log)
self.out.nl() self.out.nl()
def Execute(self, opt, args): def ValidateOptions(self, opt, args):
if not args or len(args) > 2: 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.out = _Coloring(self.manifest.globalConfig)
self.printText = self.out.nofmt_printer('text') self.printText = self.out.nofmt_printer('text')
if opt.color: if opt.color:
@ -181,15 +192,15 @@ synced and their revisions won't be found.
self.printProject = self.printAdded = self.printRemoved = self.printRevision = self.printText self.printProject = self.printAdded = self.printRemoved = self.printRevision = self.printText
manifest1 = XmlManifest(self.manifest.repodir) manifest1 = XmlManifest(self.manifest.repodir)
manifest1.Override(args[0]) manifest1.Override(args[0], load_local_manifests=False)
if len(args) == 1: if len(args) == 1:
manifest2 = self.manifest manifest2 = self.manifest
else: else:
manifest2 = XmlManifest(self.manifest.repodir) manifest2 = XmlManifest(self.manifest.repodir)
manifest2.Override(args[1]) manifest2.Override(args[1], load_local_manifests=False)
diff = manifest1.projectsDiff(manifest2) diff = manifest1.projectsDiff(manifest2)
if opt.raw: if opt.raw:
self._printRawDiff(diff) self._printRawDiff(diff)
else: else:
self._printDiff(diff, color=opt.color) self._printDiff(diff, color=opt.color, pretty_format=opt.pretty_format)

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

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2008 The Android Open Source Project # Copyright (C) 2008 The Android Open Source Project
# #
@ -26,11 +27,12 @@ class Download(Command):
common = True common = True
helpSummary = "Download and checkout a change" helpSummary = "Download and checkout a change"
helpUsage = """ helpUsage = """
%prog {project change[/patchset]}... %prog {[project] change[/patchset]}...
""" """
helpDescription = """ helpDescription = """
The '%prog' command downloads a change from the review system and The '%prog' command downloads a change from the review system and
makes it available in your project's local working directory. makes it available in your project's local working directory.
If no project is specified try to use current directory as a project.
""" """
def _Options(self, p): def _Options(self, p):
@ -55,12 +57,21 @@ makes it available in your project's local working directory.
m = CHANGE_RE.match(a) m = CHANGE_RE.match(a)
if m: if m:
if not project: if not project:
self.Usage() project = self.GetProjects(".")[0]
chg_id = int(m.group(1)) chg_id = int(m.group(1))
if m.group(2): if m.group(2):
ps_id = int(m.group(2)) ps_id = int(m.group(2))
else: else:
ps_id = 1 ps_id = 1
refs = 'refs/changes/%2.2d/%d/' % (chg_id % 100, chg_id)
output = project._LsRemote(refs + '*')
if output:
regex = refs + r'(\d+)'
rcomp = re.compile(regex, re.I)
for line in output.splitlines():
match = rcomp.search(line)
if match:
ps_id = max(int(match.group(1)), ps_id)
to_get.append((project, chg_id, ps_id)) to_get.append((project, chg_id, ps_id))
else: else:
project = self.GetProjects([a])[0] project = self.GetProjects([a])[0]

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2008 The Android Open Source Project # Copyright (C) 2008 The Android Open Source Project
# #
@ -15,17 +16,16 @@
from __future__ import print_function from __future__ import print_function
import errno import errno
import fcntl
import multiprocessing import multiprocessing
import re import re
import os import os
import select
import signal import signal
import sys import sys
import subprocess import subprocess
from color import Coloring from color import Coloring
from command import Command, MirrorSafeCommand from command import Command, MirrorSafeCommand
import platform_utils
_CAN_COLOR = [ _CAN_COLOR = [
'branch', 'branch',
@ -54,8 +54,7 @@ Executes the same shell command in each project.
The -r option allows running the command only on projects matching The -r option allows running the command only on projects matching
regex or wildcard expression. regex or wildcard expression.
Output Formatting # Output Formatting
-----------------
The -p option causes '%prog' to bind pipes to the command's stdin, The -p option causes '%prog' to bind pipes to the command's stdin,
stdout and stderr streams, and pipe all output into a continuous stdout and stderr streams, and pipe all output into a continuous
@ -72,8 +71,7 @@ command produces output only on stderr. Normally the -p option
causes command output to be suppressed until the command produces causes command output to be suppressed until the command produces
at least one byte of output on stdout. at least one byte of output on stdout.
Environment # Environment
-----------
pwd is the project's working directory. If the current client is pwd is the project's working directory. If the current client is
a mirror client, then pwd is the Git repository. a mirror client, then pwd is the Git repository.
@ -105,6 +103,13 @@ annotating tree details.
shell positional arguments ($1, $2, .., $#) are set to any arguments shell positional arguments ($1, $2, .., $#) are set to any arguments
following <command>. following <command>.
Example: to list projects:
%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.
Unless -p is used, stdin, stdout, stderr are inherited from the Unless -p is used, stdin, stdout, stderr are inherited from the
terminal and are not redirected. terminal and are not redirected.
@ -120,6 +125,9 @@ without iterating through the remaining projects.
p.add_option('-r', '--regex', p.add_option('-r', '--regex',
dest='regex', action='store_true', dest='regex', action='store_true',
help="Execute the command only on projects matching regex or wildcard expression") help="Execute the command only on projects matching regex or wildcard expression")
p.add_option('-i', '--inverse-regex',
dest='inverse_regex', action='store_true',
help="Execute the command only on projects not matching regex or wildcard expression")
p.add_option('-g', '--groups', p.add_option('-g', '--groups',
dest='groups', dest='groups',
help="Execute the command only on projects matching the specified groups") help="Execute the command only on projects matching the specified groups")
@ -169,10 +177,11 @@ without iterating through the remaining projects.
'worktree': project.worktree, 'worktree': project.worktree,
} }
def Execute(self, opt, args): def ValidateOptions(self, opt, args):
if not opt.command: if not opt.command:
self.Usage() self.Usage()
def Execute(self, opt, args):
cmd = [opt.command[0]] cmd = [opt.command[0]]
shell = True shell = True
@ -196,14 +205,12 @@ without iterating through the remaining projects.
break break
else: else:
cn = None cn = None
# pylint: disable=W0631
if cn and cn in _CAN_COLOR: if cn and cn in _CAN_COLOR:
class ColorCmd(Coloring): class ColorCmd(Coloring):
def __init__(self, config, cmd): def __init__(self, config, cmd):
Coloring.__init__(self, config, cmd) Coloring.__init__(self, config, cmd)
if ColorCmd(self.manifest.manifestProject.config, cn).is_on: if ColorCmd(self.manifest.manifestProject.config, cn).is_on:
cmd.insert(cmd.index(cn) + 1, '--color') cmd.insert(cmd.index(cn) + 1, '--color')
# pylint: enable=W0631
mirror = self.manifest.IsMirror mirror = self.manifest.IsMirror
rc = 0 rc = 0
@ -215,10 +222,12 @@ without iterating through the remaining projects.
if os.path.isfile(smart_sync_manifest_path): if os.path.isfile(smart_sync_manifest_path):
self.manifest.Override(smart_sync_manifest_path) self.manifest.Override(smart_sync_manifest_path)
if not opt.regex: if opt.regex:
projects = self.GetProjects(args, groups=opt.groups)
else:
projects = self.FindProjects(args) projects = self.FindProjects(args)
elif opt.inverse_regex:
projects = self.FindProjects(args, inverse=True)
else:
projects = self.GetProjects(args, groups=opt.groups)
os.environ['REPO_COUNT'] = str(len(projects)) os.environ['REPO_COUNT'] = str(len(projects))
@ -339,35 +348,25 @@ def DoWork(project, mirror, opt, cmd, shell, cnt, config):
if opt.project_header: if opt.project_header:
out = ForallColoring(config) out = ForallColoring(config)
out.redirect(sys.stdout) out.redirect(sys.stdout)
class sfd(object):
def __init__(self, fd, dest):
self.fd = fd
self.dest = dest
def fileno(self):
return self.fd.fileno()
empty = True empty = True
errbuf = '' errbuf = ''
p.stdin.close() p.stdin.close()
s_in = [sfd(p.stdout, sys.stdout), s_in = platform_utils.FileDescriptorStreams.create()
sfd(p.stderr, sys.stderr)] s_in.add(p.stdout, sys.stdout, 'stdout')
s_in.add(p.stderr, sys.stderr, 'stderr')
for s in s_in: while not s_in.is_done:
flags = fcntl.fcntl(s.fd, fcntl.F_GETFL) in_ready = s_in.select()
fcntl.fcntl(s.fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
while s_in:
in_ready, _out_ready, _err_ready = select.select(s_in, [], [])
for s in in_ready: for s in in_ready:
buf = s.fd.read(4096) buf = s.read()
if not buf: if not buf:
s.fd.close() s.close()
s_in.remove(s) s_in.remove(s)
continue continue
if not opt.verbose: if not opt.verbose:
if s.fd != p.stdout: if s.std_name == 'stderr':
errbuf += buf errbuf += buf
continue continue

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2015 The Android Open Source Project # Copyright (C) 2015 The Android Open Source Project
# #
@ -14,18 +15,14 @@
# limitations under the License. # limitations under the License.
from __future__ import print_function from __future__ import print_function
import os
import shutil
import sys import sys
from command import Command, GitcClientCommand from command import Command, GitcClientCommand
import gitc_utils import platform_utils
from pyversion import is_python3 from pyversion import is_python3
if not is_python3(): if not is_python3():
# pylint:disable=W0622
input = raw_input input = raw_input
# pylint:enable=W0622
class GitcDelete(Command, GitcClientCommand): class GitcDelete(Command, GitcClientCommand):
common = True common = True
@ -52,4 +49,4 @@ and all locally downloaded sources.
if not response == 'yes': if not response == 'yes':
print('Response was not "yes"\n Exiting...') print('Response was not "yes"\n Exiting...')
sys.exit(1) sys.exit(1)
shutil.rmtree(self.gitc_manifest.gitc_client_dir) platform_utils.rmtree(self.gitc_manifest.gitc_client_dir)

View File

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

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2009 The Android Open Source Project # Copyright (C) 2009 The Android Open Source Project
# #
@ -33,8 +34,7 @@ class Grep(PagedCommand):
helpDescription = """ helpDescription = """
Search for the specified patterns in all project files. Search for the specified patterns in all project files.
Boolean Options # Boolean Options
---------------
The following options can appear as often as necessary to express The following options can appear as often as necessary to express
the pattern to locate: the pattern to locate:
@ -47,8 +47,7 @@ in order to scan multiple trees. If the same file matches in more
than one tree, only the first result is reported, prefixed by the than one tree, only the first result is reported, prefixed by the
revision name it was found under. revision name it was found under.
Examples # Examples
-------
Look for a line that has '#define' and either 'MAX_PATH or 'PATH_MAX': Look for a line that has '#define' and either 'MAX_PATH or 'PATH_MAX':

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2008 The Android Open Source Project # Copyright (C) 2008 The Android Open Source Project
# #
@ -87,7 +88,7 @@ Displays detailed usage information about a command.
"See 'repo help <command>' for more information on a specific command.\n" "See 'repo help <command>' for more information on a specific command.\n"
"See 'repo help --all' for a complete list of recognized commands.") "See 'repo help --all' for a complete list of recognized commands.")
def _PrintCommandHelp(self, cmd): def _PrintCommandHelp(self, cmd, header_prefix=''):
class _Out(Coloring): class _Out(Coloring):
def __init__(self, gc): def __init__(self, gc):
Coloring.__init__(self, gc, 'help') Coloring.__init__(self, gc, 'help')
@ -105,17 +106,15 @@ Displays detailed usage information about a command.
self.nl() self.nl()
self.heading('%s', heading) self.heading('%s%s', header_prefix, heading)
self.nl() self.nl()
self.heading('%s', ''.ljust(len(heading), '-'))
self.nl() self.nl()
me = 'repo %s' % cmd.NAME me = 'repo %s' % cmd.NAME
body = body.strip() body = body.strip()
body = body.replace('%prog', me) body = body.replace('%prog', me)
asciidoc_hdr = re.compile(r'^\n?([^\n]{1,})\n([=~-]{2,})$') asciidoc_hdr = re.compile(r'^\n?#+ (.+)$')
for para in body.split("\n\n"): for para in body.split("\n\n"):
if para.startswith(' '): if para.startswith(' '):
self.write('%s', para) self.write('%s', para)
@ -125,19 +124,8 @@ Displays detailed usage information about a command.
m = asciidoc_hdr.match(para) m = asciidoc_hdr.match(para)
if m: if m:
title = m.group(1) self.heading('%s%s', header_prefix, m.group(1))
section_type = m.group(2)
if section_type[0] in ('=', '-'):
p = self.heading
else:
def _p(fmt, *args):
self.write(' ')
self.heading(fmt, *args)
p = _p
p('%s', title)
self.nl() self.nl()
p('%s', ''.ljust(len(title), section_type[0]))
self.nl() self.nl()
continue continue
@ -150,14 +138,25 @@ Displays detailed usage information about a command.
cmd.OptionParser.print_help() cmd.OptionParser.print_help()
out._PrintSection('Description', 'helpDescription') 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): def _Options(self, p):
p.add_option('-a', '--all', p.add_option('-a', '--all',
dest='show_all', action='store_true', dest='show_all', action='store_true',
help='show the complete list of commands') 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): def Execute(self, opt, args):
if len(args) == 0: if len(args) == 0:
if opt.show_all: if opt.show_all_help:
self._PrintAllCommandHelp()
elif opt.show_all:
self._PrintAllCommands() self._PrintAllCommands()
else: else:
self._PrintCommonCommands() self._PrintCommonCommands()

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2012 The Android Open Source Project # Copyright (C) 2012 The Android Open Source Project
# #
@ -45,7 +46,7 @@ class Info(PagedCommand):
def Execute(self, opt, args): def Execute(self, opt, args):
self.out = _Coloring(self.manifest.globalConfig) self.out = _Coloring(self.manifest.globalConfig)
self.heading = self.out.printer('heading', attr = 'bold') self.heading = self.out.printer('heading', attr = 'bold')
self.headtext = self.out.printer('headtext', fg = 'yellow') self.headtext = self.out.nofmt_printer('headtext', fg = 'yellow')
self.redtext = self.out.printer('redtext', fg = 'red') self.redtext = self.out.printer('redtext', fg = 'red')
self.sha = self.out.printer("sha", fg = 'yellow') self.sha = self.out.printer("sha", fg = 'yellow')
self.text = self.out.nofmt_printer('text') self.text = self.out.nofmt_printer('text')
@ -99,7 +100,7 @@ class Info(PagedCommand):
self.headtext(p.revisionExpr) self.headtext(p.revisionExpr)
self.out.nl() self.out.nl()
localBranches = p.GetBranches().keys() localBranches = list(p.GetBranches().keys())
self.heading("Local Branches: ") self.heading("Local Branches: ")
self.redtext(str(len(localBranches))) self.redtext(str(len(localBranches)))
if len(localBranches) > 0: if len(localBranches) > 0:

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2008 The Android Open Source Project # Copyright (C) 2008 The Android Open Source Project
# #
@ -17,7 +18,6 @@ from __future__ import print_function
import os import os
import platform import platform
import re import re
import shutil
import sys import sys
from pyversion import is_python3 from pyversion import is_python3
@ -35,6 +35,7 @@ from error import ManifestParseError
from project import SyncBuffer from project import SyncBuffer
from git_config import GitConfig from git_config import GitConfig
from git_command import git_require, MIN_GIT_VERSION from git_command import git_require, MIN_GIT_VERSION
import platform_utils
class Init(InteractiveCommand, MirrorSafeCommand): class Init(InteractiveCommand, MirrorSafeCommand):
common = True common = True
@ -61,9 +62,18 @@ directory use as much data as possible from the local reference
directory when fetching from the server. This will make the sync directory when fetching from the server. This will make the sync
go a lot faster by reducing data traffic on the network. go a lot faster by reducing data traffic on the network.
The --dissociate option can be used to borrow the objects from
the directory specified with the --reference option only to reduce
network transfer, and stop borrowing from them after a first clone
is made by making necessary local copies of borrowed objects.
Switching Manifest Branches 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
may be necessary if there are problems with the local Python
HTTP client or proxy configuration, but the Git binary works.
# Switching Manifest Branches
To switch to another manifest branch, `repo init -b otherbranch` To switch to another manifest branch, `repo init -b otherbranch`
may be used in an existing client. However, as this only updates the may be used in an existing client. However, as this only updates the
@ -86,6 +96,9 @@ to update the working directory files.
g.add_option('-b', '--manifest-branch', g.add_option('-b', '--manifest-branch',
dest='manifest_branch', dest='manifest_branch',
help='manifest branch or revision', metavar='REVISION') help='manifest branch or revision', metavar='REVISION')
g.add_option('--current-branch',
dest='current_branch_only', action='store_true',
help='fetch only current manifest branch from server')
g.add_option('-m', '--manifest-name', g.add_option('-m', '--manifest-name',
dest='manifest_name', default='default.xml', dest='manifest_name', default='default.xml',
help='initial manifest file', metavar='NAME.xml') help='initial manifest file', metavar='NAME.xml')
@ -96,13 +109,26 @@ to update the working directory files.
g.add_option('--reference', g.add_option('--reference',
dest='reference', dest='reference',
help='location of mirror directory', metavar='DIR') help='location of mirror directory', metavar='DIR')
g.add_option('--dissociate',
dest='dissociate', action='store_true',
help='dissociate from reference mirrors after clone')
g.add_option('--depth', type='int', default=None, g.add_option('--depth', type='int', default=None,
dest='depth', dest='depth',
help='create a shallow clone with given depth; see git clone') 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', g.add_option('--archive',
dest='archive', action='store_true', dest='archive', action='store_true',
help='checkout an archive instead of a git repository for ' help='checkout an archive instead of a git repository for '
'each project. See git archive.') 'each project. See git archive.')
g.add_option('--submodules',
dest='submodules', action='store_true',
help='sync any submodules associated with the manifest repo')
g.add_option('-g', '--groups', g.add_option('-g', '--groups',
dest='groups', default='default', dest='groups', default='default',
help='restrict manifest projects to ones with specified ' help='restrict manifest projects to ones with specified '
@ -113,6 +139,12 @@ to update the working directory files.
help='restrict manifest projects to ones with a specified ' help='restrict manifest projects to ones with a specified '
'platform group [auto|all|none|linux|darwin|...]', 'platform group [auto|all|none|linux|darwin|...]',
metavar='PLATFORM') metavar='PLATFORM')
g.add_option('--no-clone-bundle',
dest='no_clone_bundle', action='store_true',
help='disable use of /clone.bundle on HTTP/HTTPS')
g.add_option('--no-tags',
dest='no_tags', action='store_true',
help="don't fetch tags in the manifest")
# Tool # Tool
g = p.add_option_group('repo Version options') g = p.add_option_group('repo Version options')
@ -158,7 +190,8 @@ to update the working directory files.
if not mirrored_manifest_git.endswith(".git"): if not mirrored_manifest_git.endswith(".git"):
mirrored_manifest_git += ".git" mirrored_manifest_git += ".git"
if not os.path.exists(mirrored_manifest_git): if not os.path.exists(mirrored_manifest_git):
mirrored_manifest_git = os.path.join(opt.reference + '/.repo/manifests.git') mirrored_manifest_git = os.path.join(opt.reference,
'.repo/manifests.git')
m._InitGitDir(mirror_git=mirrored_manifest_git) m._InitGitDir(mirror_git=mirrored_manifest_git)
@ -172,6 +205,8 @@ to update the working directory files.
else: else:
m.PreSync() m.PreSync()
self._ConfigureDepth(opt)
if opt.manifest_url: if opt.manifest_url:
r = m.GetRemote(m.remote.name) r = m.GetRemote(m.remote.name)
r.url = opt.manifest_url r.url = opt.manifest_url
@ -202,6 +237,9 @@ to update the working directory files.
if opt.reference: if opt.reference:
m.config.SetString('repo.reference', opt.reference) m.config.SetString('repo.reference', opt.reference)
if opt.dissociate:
m.config.SetString('repo.dissociate', 'true')
if opt.archive: if opt.archive:
if is_new: if is_new:
m.config.SetString('repo.archive', 'true') m.config.SetString('repo.archive', 'true')
@ -222,21 +260,39 @@ to update the working directory files.
'in another location.', file=sys.stderr) 'in another location.', file=sys.stderr)
sys.exit(1) sys.exit(1)
if not m.Sync_NetworkHalf(is_new=is_new): 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,
clone_filter=opt.clone_filter):
r = m.GetRemote(m.remote.name) r = m.GetRemote(m.remote.name)
print('fatal: cannot obtain manifest %s' % r.url, file=sys.stderr) print('fatal: cannot obtain manifest %s' % r.url, file=sys.stderr)
# Better delete the manifest git dir if we created it; otherwise next # Better delete the manifest git dir if we created it; otherwise next
# time (when user fixes problems) we won't go through the "is_new" logic. # time (when user fixes problems) we won't go through the "is_new" logic.
if is_new: if is_new:
shutil.rmtree(m.gitdir) platform_utils.rmtree(m.gitdir)
sys.exit(1) sys.exit(1)
if opt.manifest_branch: if opt.manifest_branch:
m.MetaBranchSwitch() m.MetaBranchSwitch(submodules=opt.submodules)
syncbuf = SyncBuffer(m.config) syncbuf = SyncBuffer(m.config)
m.Sync_LocalHalf(syncbuf) m.Sync_LocalHalf(syncbuf, submodules=opt.submodules)
syncbuf.Finish() syncbuf.Finish()
if is_new or m.CurrentBranch is None: if is_new or m.CurrentBranch is None:
@ -257,7 +313,9 @@ to update the working directory files.
sys.exit(1) sys.exit(1)
def _Prompt(self, prompt, value): 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() a = sys.stdin.readline().strip()
if a == '': if a == '':
return value return value
@ -291,7 +349,9 @@ to update the working directory files.
print() print()
print('Your identity is: %s <%s>' % (name, email)) 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() a = sys.stdin.readline().strip().lower()
if a in ('yes', 'y', 't', 'true'): if a in ('yes', 'y', 't', 'true'):
break break
@ -333,7 +393,9 @@ to update the working directory files.
out.printer(fg='black', attr=c)(' %-6s ', c) out.printer(fg='black', attr=c)(' %-6s ', c)
out.nl() 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() a = sys.stdin.readline().strip().lower()
if a in ('y', 'yes', 't', 'true', 'on'): if a in ('y', 'yes', 't', 'true', 'on'):
gc.SetString('color.ui', 'auto') gc.SetString('color.ui', 'auto')
@ -374,18 +436,17 @@ to update the working directory files.
print(' rm -r %s/.repo' % self.manifest.topdir) print(' rm -r %s/.repo' % self.manifest.topdir)
print('and try again.') print('and try again.')
def Execute(self, opt, args): def ValidateOptions(self, opt, args):
git_require(MIN_GIT_VERSION, fail=True)
if opt.reference: if opt.reference:
opt.reference = os.path.expanduser(opt.reference) opt.reference = os.path.expanduser(opt.reference)
# Check this here, else manifest will be tagged "not new" and init won't be # Check this here, else manifest will be tagged "not new" and init won't be
# possible anymore without removing the .repo/manifests directory. # possible anymore without removing the .repo/manifests directory.
if opt.archive and opt.mirror: 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.')
file=sys.stderr)
sys.exit(1) def Execute(self, opt, args):
git_require(MIN_GIT_VERSION, fail=True)
self._SyncManifest(opt) self._SyncManifest(opt)
self._LinkManifest(opt.manifest_name) self._LinkManifest(opt.manifest_name)
@ -395,6 +456,4 @@ to update the working directory files.
self._ConfigureUser() self._ConfigureUser()
self._ConfigureColor() self._ConfigureColor()
self._ConfigureDepth(opt)
self._DisplayResult() self._DisplayResult()

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2011 The Android Open Source Project # 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', dest='path_only', action='store_true',
help="Display only the path of the repository") 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): def Execute(self, opt, args):
"""List all projects and the associated directories. """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. opt: The options.
args: Positional args. Can be a list of projects to list, or empty. 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: if not opt.regex:
projects = self.GetProjects(args, groups=opt.groups) projects = self.GetProjects(args, groups=opt.groups)
else: else:

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2009 The Android Open Source Project # Copyright (C) 2009 The Android Open Source Project
# #
@ -39,7 +40,7 @@ in a Git repository for use during future 'repo init' invocations.
helptext = self._helpDescription + '\n' helptext = self._helpDescription + '\n'
r = os.path.dirname(__file__) r = os.path.dirname(__file__)
r = os.path.dirname(r) r = os.path.dirname(r)
fd = open(os.path.join(r, 'docs', 'manifest-format.txt')) fd = open(os.path.join(r, 'docs', 'manifest-format.md'))
for line in fd: for line in fd:
helptext += line helptext += line
fd.close() fd.close()
@ -72,14 +73,9 @@ in a Git repository for use during future 'repo init' invocations.
if opt.output_file != '-': if opt.output_file != '-':
print('Saved manifest to %s' % opt.output_file, file=sys.stderr) print('Saved manifest to %s' % opt.output_file, file=sys.stderr)
def Execute(self, opt, args): def ValidateOptions(self, opt, args):
if args: if args:
self.Usage() self.Usage()
if opt.output_file is not None: def Execute(self, opt, args):
self._Output(opt) 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)

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2012 The Android Open Source Project # 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 # Copyright (C) 2008 The Android Open Source Project
# #

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2010 The Android Open Source Project # Copyright (C) 2010 The Android Open Source Project
# #
@ -70,7 +71,22 @@ branch but need to incorporate new upstream changes "underneath" them.
if len(args) == 1: if len(args) == 1:
print('note: project %s is mapped to more than one path' % (args[0],), print('note: project %s is mapped to more than one path' % (args[0],),
file=sys.stderr) file=sys.stderr)
return -1 return 1
# Setup the common git rebase args that we use for all projects.
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')
for project in all_projects: for project in all_projects:
cb = project.CurrentBranch cb = project.CurrentBranch
@ -78,7 +94,7 @@ branch but need to incorporate new upstream changes "underneath" them.
if one_project: if one_project:
print("error: project %s has a detached HEAD" % project.relpath, print("error: project %s has a detached HEAD" % project.relpath,
file=sys.stderr) file=sys.stderr)
return -1 return 1
# ignore branches with detatched HEADs # ignore branches with detatched HEADs
continue continue
@ -87,30 +103,11 @@ branch but need to incorporate new upstream changes "underneath" them.
if one_project: if one_project:
print("error: project %s does not track any remote branches" print("error: project %s does not track any remote branches"
% project.relpath, file=sys.stderr) % project.relpath, file=sys.stderr)
return -1 return 1
# ignore branches without remotes # ignore branches without remotes
continue continue
args = ["rebase"] args = common_args[:]
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")
if opt.onto_manifest: if opt.onto_manifest:
args.append('--onto') args.append('--onto')
args.append(project.revisionExpr) args.append(project.revisionExpr)
@ -130,13 +127,13 @@ branch but need to incorporate new upstream changes "underneath" them.
stash_args = ["stash"] stash_args = ["stash"]
if GitCommand(project, stash_args).Wait() != 0: if GitCommand(project, stash_args).Wait() != 0:
return -1 return 1
if GitCommand(project, args).Wait() != 0: if GitCommand(project, args).Wait() != 0:
return -1 return 1
if needs_stash: if needs_stash:
stash_args.append('pop') stash_args.append('pop')
stash_args.append('--quiet') stash_args.append('--quiet')
if GitCommand(project, stash_args).Wait() != 0: if GitCommand(project, stash_args).Wait() != 0:
return -1 return 1

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2009 The Android Open Source Project # 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 # 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 # Copyright (C) 2008 The Android Open Source Project
# #
@ -60,8 +61,8 @@ The '%prog' command stages files to prepare the next commit.
out.nl() out.nl()
for i in range(len(all_projects)): for i in range(len(all_projects)):
p = all_projects[i] project = all_projects[i]
out.write('%3d: %s', i + 1, p.relpath + '/') out.write('%3d: %s', i + 1, project.relpath + '/')
out.nl() out.nl()
out.nl() out.nl()

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2008 The Android Open Source Project # Copyright (C) 2008 The Android Open Source Project
# #
@ -18,7 +19,7 @@ import os
import sys import sys
from command import Command from command import Command
from git_config import IsId from git_config import IsImmutable
from git_command import git from git_command import git
import gitc_utils import gitc_utils
from progress import Progress from progress import Progress
@ -39,23 +40,27 @@ revision specified in the manifest.
p.add_option('--all', p.add_option('--all',
dest='all', action='store_true', dest='all', action='store_true',
help='begin branch in all projects') 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: if not args:
self.Usage() self.Usage()
nb = args[0] nb = args[0]
if not git.check_ref_format('heads/%s' % nb): if not git.check_ref_format('heads/%s' % nb):
print("error: '%s' is not a valid name" % nb, file=sys.stderr) self.OptionParser.error("'%s' is not a valid name" % nb)
sys.exit(1)
def Execute(self, opt, args):
nb = args[0]
err = [] err = []
projects = [] projects = []
if not opt.all: if not opt.all:
projects = args[1:] projects = args[1:]
if len(projects) < 1: if len(projects) < 1:
print("error: at least one project must be specified", file=sys.stderr) projects = ['.',] # start it in the local project by default
sys.exit(1)
all_projects = self.GetProjects(projects, all_projects = self.GetProjects(projects,
missing_ok=bool(self.gitc_manifest)) missing_ok=bool(self.gitc_manifest))
@ -97,17 +102,18 @@ revision specified in the manifest.
project.Sync_LocalHalf(sync_buf) project.Sync_LocalHalf(sync_buf)
project.revisionId = gitc_project.old_revision project.revisionId = gitc_project.old_revision
# If the current revision is a specific SHA1 then we can't push back # If the current revision is immutable, such as a SHA1, a tag or
# to it; so substitute with dest_branch if defined, or with manifest # a change, then we can't push back to it. Substitute with
# default revision instead. # dest_branch, if defined; or with manifest default revision instead.
branch_merge = '' branch_merge = ''
if IsId(project.revisionExpr): if IsImmutable(project.revisionExpr):
if project.dest_branch: if project.dest_branch:
branch_merge = project.dest_branch branch_merge = project.dest_branch
else: else:
branch_merge = self.manifest.default.revisionExpr 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) err.append(project)
pm.end() pm.end()

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2008 The Android Open Source Project # Copyright (C) 2008 The Android Open Source Project
# #
@ -13,6 +14,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from __future__ import print_function
from command import PagedCommand from command import PagedCommand
try: try:
@ -26,6 +29,7 @@ import itertools
import os import os
from color import Coloring from color import Coloring
import platform_utils
class Status(PagedCommand): class Status(PagedCommand):
common = True common = True
@ -49,8 +53,7 @@ includes deeper items. For example, if dir/subdir/proj1 and
dir/subdir/proj2 are repo projects, dir/subdir/proj3 will be shown dir/subdir/proj2 are repo projects, dir/subdir/proj3 will be shown
if it is not known to repo. if it is not known to repo.
Status Display # Status Display
--------------
The status display is organized into three columns of information, The status display is organized into three columns of information,
for example if the file 'subcmds/status.py' is modified in the for example if the file 'subcmds/status.py' is modified in the
@ -89,8 +92,10 @@ the following meanings:
p.add_option('-o', '--orphans', p.add_option('-o', '--orphans',
dest='orphans', action='store_true', dest='orphans', action='store_true',
help="include objects in working directory outside of repo projects") help="include objects in working directory outside of repo projects")
p.add_option('-q', '--quiet', action='store_true',
help="only print the name of modified projects")
def _StatusHelper(self, project, clean_counter, sem): def _StatusHelper(self, project, clean_counter, sem, quiet):
"""Obtains the status for a specific project. """Obtains the status for a specific project.
Obtains the status for a project, redirecting the output to Obtains the status for a project, redirecting the output to
@ -104,7 +109,7 @@ the following meanings:
output: Where to output the status. output: Where to output the status.
""" """
try: try:
state = project.PrintWorkTreeStatus() state = project.PrintWorkTreeStatus(quiet=quiet)
if state == 'CLEAN': if state == 'CLEAN':
next(clean_counter) next(clean_counter)
finally: finally:
@ -114,7 +119,7 @@ the following meanings:
"""find 'dirs' that are present in 'proj_dirs_parents' but not in 'proj_dirs'""" """find 'dirs' that are present in 'proj_dirs_parents' but not in 'proj_dirs'"""
status_header = ' --\t' status_header = ' --\t'
for item in dirs: for item in dirs:
if not os.path.isdir(item): if not platform_utils.isdir(item):
outstring.append(''.join([status_header, item])) outstring.append(''.join([status_header, item]))
continue continue
if item in proj_dirs: if item in proj_dirs:
@ -132,7 +137,7 @@ the following meanings:
if opt.jobs == 1: if opt.jobs == 1:
for project in all_projects: for project in all_projects:
state = project.PrintWorkTreeStatus() state = project.PrintWorkTreeStatus(quiet=opt.quiet)
if state == 'CLEAN': if state == 'CLEAN':
next(counter) next(counter)
else: else:
@ -142,13 +147,13 @@ the following meanings:
sem.acquire() sem.acquire()
t = _threading.Thread(target=self._StatusHelper, t = _threading.Thread(target=self._StatusHelper,
args=(project, counter, sem)) args=(project, counter, sem, opt.quiet))
threads.append(t) threads.append(t)
t.daemon = True t.daemon = True
t.start() t.start()
for t in threads: for t in threads:
t.join() t.join()
if len(all_projects) == next(counter): if not opt.quiet and len(all_projects) == next(counter):
print('nothing to commit (working directory clean)') print('nothing to commit (working directory clean)')
if opt.orphans: if opt.orphans:

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2008 The Android Open Source Project # Copyright (C) 2008 The Android Open Source Project
# #
@ -19,7 +20,6 @@ import netrc
from optparse import SUPPRESS_HELP from optparse import SUPPRESS_HELP
import os import os
import re import re
import shutil
import socket import socket
import subprocess import subprocess
import sys import sys
@ -64,6 +64,7 @@ try:
except ImportError: except ImportError:
multiprocessing = None multiprocessing = None
import event_log
from git_command import GIT, git_require from git_command import GIT, git_require
from git_config import GetUrlCookieFile from git_config import GetUrlCookieFile
from git_refs import R_HEADS, HEAD from git_refs import R_HEADS, HEAD
@ -72,6 +73,7 @@ from project import Project
from project import RemoteSpec from project import RemoteSpec
from command import Command, MirrorSafeCommand from command import Command, MirrorSafeCommand
from error import RepoChangedException, GitError, ManifestParseError from error import RepoChangedException, GitError, ManifestParseError
import platform_utils
from project import SyncBuffer from project import SyncBuffer
from progress import Progress from progress import Progress
from wrapper import Wrapper from wrapper import Wrapper
@ -83,6 +85,9 @@ class _FetchError(Exception):
"""Internal error thrown in _FetchHelper() when we don't want stack trace.""" """Internal error thrown in _FetchHelper() when we don't want stack trace."""
pass pass
class _CheckoutError(Exception):
"""Internal error thrown in _CheckoutOne() when we don't want stack trace."""
class Sync(Command, MirrorSafeCommand): class Sync(Command, MirrorSafeCommand):
jobs = 1 jobs = 1
common = True common = True
@ -127,14 +132,19 @@ from the user's .netrc file.
if the manifest server specified in the manifest file already includes if the manifest server specified in the manifest file already includes
credentials. credentials.
The -f/--force-broken option can be used to proceed with syncing By default, all projects will be synced. The --fail-fast option can be used
other projects if a project sync fails. 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 The --force-sync option can be used to overwrite existing git
directories if they have previously been linked to a different directories if they have previously been linked to a different
object direcotry. WARNING: This may cause data to be lost since object direcotry. WARNING: This may cause data to be lost since
refs may be removed when overwriting. 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 The --no-clone-bundle option disables any attempt to use
$URL/clone.bundle to bootstrap a new Git repository from a $URL/clone.bundle to bootstrap a new Git repository from a
resumeable bundle file on a content delivery network. This resumeable bundle file on a content delivery network. This
@ -154,8 +164,7 @@ exist locally.
The --prune option can be used to remove any refs that no longer The --prune option can be used to remove any refs that no longer
exist on the remote. exist on the remote.
SSH Connections # SSH Connections
---------------
If at least one project remote URL uses an SSH connection (ssh://, If at least one project remote URL uses an SSH connection (ssh://,
git+ssh://, or user@host:path syntax) repo will automatically git+ssh://, or user@host:path syntax) repo will automatically
@ -169,8 +178,7 @@ environment variable to 'ssh'. For example:
export GIT_SSH=ssh export GIT_SSH=ssh
%prog %prog
Compatibility # Compatibility
~~~~~~~~~~~~~
This feature is automatically disabled on Windows, due to the lack This feature is automatically disabled on Windows, due to the lack
of UNIX domain socket support. of UNIX domain socket support.
@ -192,12 +200,20 @@ later is required to fix a server side protocol bug.
p.add_option('-f', '--force-broken', p.add_option('-f', '--force-broken',
dest='force_broken', action='store_true', 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', p.add_option('--force-sync',
dest='force_sync', action='store_true', dest='force_sync', action='store_true',
help="overwrite an existing git directory if it needs to " help="overwrite an existing git directory if it needs to "
"point to a different object directory. WARNING: this " "point to a different object directory. WARNING: this "
"may cause loss of data") "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', p.add_option('-l', '--local-only',
dest='local_only', action='store_true', dest='local_only', action='store_true',
help="only update working tree, don't fetch") help="only update working tree, don't fetch")
@ -242,7 +258,7 @@ later is required to fix a server side protocol bug.
if show_smart: if show_smart:
p.add_option('-s', '--smart-sync', p.add_option('-s', '--smart-sync',
dest='smart_sync', action='store_true', dest='smart_sync', action='store_true',
help='smart sync using manifest from a known good build') help='smart sync using manifest from the latest known good build')
p.add_option('-t', '--smart-tag', p.add_option('-t', '--smart-tag',
dest='smart_tag', action='store', dest='smart_tag', action='store',
help='smart sync using manifest from a known tag') help='smart sync using manifest from a known tag')
@ -255,23 +271,29 @@ later is required to fix a server side protocol bug.
dest='repo_upgraded', action='store_true', dest='repo_upgraded', action='store_true',
help=SUPPRESS_HELP) help=SUPPRESS_HELP)
def _FetchProjectList(self, opt, projects, *args, **kwargs): 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. Delegates most of the work to _FetchHelper.
Args: Args:
opt: Program options returned from optparse. See _Options(). opt: Program options returned from optparse. See _Options().
projects: Projects to fetch. 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 _FetchHelper. See the *args, **kwargs: Remaining arguments to pass to _FetchHelper. See the
_FetchHelper docstring for details. _FetchHelper docstring for details.
""" """
for project in projects: try:
success = self._FetchHelper(opt, project, *args, **kwargs) for project in projects:
if not success and not opt.force_broken: success = self._FetchHelper(opt, project, *args, **kwargs)
break if not success and opt.fail_fast:
break
finally:
sem.release()
def _FetchHelper(self, opt, project, lock, fetched, pm, sem, err_event): def _FetchHelper(self, opt, project, lock, fetched, pm, err_event,
clone_filter):
"""Fetch git objects for a single project. """Fetch git objects for a single project.
Args: Args:
@ -283,10 +305,9 @@ later is required to fix a server side protocol bug.
(with our lock held). (with our lock held).
pm: Instance of a Project object. We will call pm.update() (with our pm: Instance of a Project object. We will call pm.update() (with our
lock held). lock held).
sem: We'll release() this semaphore when we exit so that another thread
can be started up.
err_event: We'll set this event in the case of an error (after printing err_event: We'll set this event in the case of an error (after printing
out info about the error). out info about the error).
clone_filter: Filter for use in a partial clone.
Returns: Returns:
Whether the fetch was successful. Whether the fetch was successful.
@ -299,11 +320,11 @@ later is required to fix a server side protocol bug.
# Encapsulate everything in a try/except/finally so that: # Encapsulate everything in a try/except/finally so that:
# - We always set err_event in the case of an exception. # - 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. # - We always make sure we unlock the lock if we locked it.
start = time.time()
success = False
try: try:
try: try:
start = time.time()
success = project.Sync_NetworkHalf( success = project.Sync_NetworkHalf(
quiet=opt.quiet, quiet=opt.quiet,
current_branch_only=opt.current_branch_only, current_branch_only=opt.current_branch_only,
@ -311,7 +332,8 @@ later is required to fix a server side protocol bug.
clone_bundle=not opt.no_clone_bundle, clone_bundle=not opt.no_clone_bundle,
no_tags=opt.no_tags, archive=self.manifest.IsArchive, no_tags=opt.no_tags, archive=self.manifest.IsArchive,
optimized_fetch=opt.optimized_fetch, optimized_fetch=opt.optimized_fetch,
prune=opt.prune) prune=opt.prune,
clone_filter=clone_filter)
self._fetch_times.Set(project, time.time() - start) self._fetch_times.Set(project, time.time() - start)
# Lock around all the rest of the code, since printing, updating a set # Lock around all the rest of the code, since printing, updating a set
@ -321,11 +343,10 @@ later is required to fix a server side protocol bug.
if not success: if not success:
err_event.set() err_event.set()
print('error: Cannot fetch %s' % project.name, file=sys.stderr) print('error: Cannot fetch %s from %s'
if opt.force_broken: % (project.name, project.remote.url),
print('warn: --force-broken, continuing to sync', file=sys.stderr)
file=sys.stderr) if opt.fail_fast:
else:
raise _FetchError() raise _FetchError()
fetched.add(project.gitdir) fetched.add(project.gitdir)
@ -340,14 +361,18 @@ later is required to fix a server side protocol bug.
finally: finally:
if did_lock: if did_lock:
lock.release() lock.release()
sem.release() finish = time.time()
self.event_log.AddSync(project, event_log.TASK_SYNC_NETWORK,
start, finish, success)
return success return success
def _Fetch(self, projects, opt): def _Fetch(self, projects, opt):
fetched = set() fetched = set()
lock = _threading.Lock() lock = _threading.Lock()
pm = Progress('Fetching projects', len(projects)) pm = Progress('Fetching projects', len(projects),
print_newline=not(opt.quiet),
always_print_percentage=opt.quiet)
objdir_project_map = dict() objdir_project_map = dict()
for project in projects: for project in projects:
@ -359,17 +384,18 @@ later is required to fix a server side protocol bug.
for project_list in objdir_project_map.values(): for project_list in objdir_project_map.values():
# Check for any errors before running any more tasks. # Check for any errors before running any more tasks.
# ...we'll let existing threads finish, though. # ...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 break
sem.acquire() sem.acquire()
kwargs = dict(opt=opt, kwargs = dict(opt=opt,
projects=project_list, projects=project_list,
sem=sem,
lock=lock, lock=lock,
fetched=fetched, fetched=fetched,
pm=pm, pm=pm,
sem=sem, err_event=err_event,
err_event=err_event) clone_filter=self.manifest.CloneFilter)
if self.jobs > 1: if self.jobs > 1:
t = _threading.Thread(target = self._FetchProjectList, t = _threading.Thread(target = self._FetchProjectList,
kwargs = kwargs) kwargs = kwargs)
@ -384,7 +410,7 @@ later is required to fix a server side protocol bug.
t.join() t.join()
# If we saw an error, exit with code 1 so that other scripts can check. # If we saw an error, exit with code 1 so that other scripts can check.
if err_event.isSet(): if err_event.isSet() and opt.fail_fast:
print('\nerror: Exited sync due to fetch errors', file=sys.stderr) print('\nerror: Exited sync due to fetch errors', file=sys.stderr)
sys.exit(1) sys.exit(1)
@ -396,10 +422,153 @@ later is required to fix a server side protocol bug.
return fetched return fetched
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):
"""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).
Returns:
Whether the fetch was successful.
"""
# We'll set to true once we've locked the lock.
did_lock = False
if not opt.quiet:
print('Checking out 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 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)
success = syncbuf.Finish()
# Lock around all the rest of the code, since printing, updating a set
# and Progress.update() are not thread safe.
lock.acquire()
did_lock = True
if not success:
err_event.set()
print('error: Cannot checkout %s' % (project.name),
file=sys.stderr)
raise _CheckoutError()
pm.update()
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:
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):
"""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().
"""
# 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('Syncing work tree', len(all_projects))
threads = set()
sem = _threading.Semaphore(syncjobs)
err_event = _threading.Event()
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)
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()
# If we saw an error, exit with code 1 so that other scripts can check.
if err_event.isSet():
print('\nerror: Exited sync due to checkout errors', file=sys.stderr)
sys.exit(1)
def _GCProjects(self, projects): def _GCProjects(self, projects):
gitdirs = {} gc_gitdirs = {}
for project in projects: for project in projects:
gitdirs[project.gitdir] = project.bare_git 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')
gc_gitdirs[project.gitdir] = project.bare_git
has_dash_c = git_require((1, 7, 2)) has_dash_c = git_require((1, 7, 2))
if multiprocessing and has_dash_c: if multiprocessing and has_dash_c:
@ -409,11 +578,11 @@ later is required to fix a server side protocol bug.
jobs = min(self.jobs, cpu_count) jobs = min(self.jobs, cpu_count)
if jobs < 2: if jobs < 2:
for bare_git in gitdirs.values(): for bare_git in gc_gitdirs.values():
bare_git.gc('--auto') bare_git.gc('--auto')
return 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() threads = set()
sem = _threading.Semaphore(jobs) sem = _threading.Semaphore(jobs)
@ -431,7 +600,7 @@ later is required to fix a server side protocol bug.
finally: finally:
sem.release() sem.release()
for bare_git in gitdirs.values(): for bare_git in gc_gitdirs.values():
if err_event.isSet(): if err_event.isSet():
break break
sem.acquire() sem.acquire()
@ -454,7 +623,66 @@ later is required to fix a server side protocol bug.
else: else:
self.manifest._Unload() self.manifest._Unload()
def UpdateProjectList(self): def _DeleteProject(self, path):
print('Deleting obsolete path %s' % path, file=sys.stderr)
# Delete the .git directory first, so we're less likely to have a partially
# working git repository around. There shouldn't be any git projects here,
# so rmtree works.
try:
platform_utils.rmtree(os.path.join(path, '.git'))
except OSError as e:
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
# Delete everything under the worktree, except for directories that contain
# another git project
dirs_to_remove = []
failed = False
for root, dirs, files in platform_utils.walk(path):
for f in files:
try:
platform_utils.remove(os.path.join(root, f))
except OSError as e:
print('Failed to remove %s (%s)' % (os.path.join(root, f), str(e)), file=sys.stderr)
failed = True
dirs[:] = [d for d in dirs
if not os.path.lexists(os.path.join(root, d, '.git'))]
dirs_to_remove += [os.path.join(root, d) for d in dirs
if os.path.join(root, d) not in dirs_to_remove]
for d in reversed(dirs_to_remove):
if platform_utils.islink(d):
try:
platform_utils.remove(d)
except OSError as e:
print('Failed to remove %s (%s)' % (os.path.join(root, d), str(e)), file=sys.stderr)
failed = True
elif len(platform_utils.listdir(d)) == 0:
try:
platform_utils.rmdir(d)
except OSError as e:
print('Failed to remove %s (%s)' % (os.path.join(root, d), str(e)), file=sys.stderr)
failed = True
continue
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
# Try deleting parent dirs if they are empty
project_dir = path
while project_dir != self.manifest.topdir:
if len(platform_utils.listdir(project_dir)) == 0:
platform_utils.rmdir(project_dir)
else:
break
project_dir = os.path.dirname(project_dir)
return 0
def UpdateProjectList(self, opt):
new_project_paths = [] new_project_paths = []
for project in self.GetProjects(None, missing_ok=True): for project in self.GetProjects(None, missing_ok=True):
if project.relpath: if project.relpath:
@ -469,13 +697,14 @@ later is required to fix a server side protocol bug.
old_project_paths = fd.read().split('\n') old_project_paths = fd.read().split('\n')
finally: finally:
fd.close() 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: if not path:
continue continue
if path not in new_project_paths: if path not in new_project_paths:
# If the path has already been deleted, we don't need to do it # If the path has already been deleted, we don't need to do it
if os.path.exists(self.manifest.topdir + '/' + path): gitdir = os.path.join(self.manifest.topdir, path, '.git')
gitdir = os.path.join(self.manifest.topdir, path, '.git') if os.path.exists(gitdir):
project = Project( project = Project(
manifest = self.manifest, manifest = self.manifest,
name = path, name = path,
@ -488,24 +717,18 @@ later is required to fix a server side protocol bug.
revisionId = None, revisionId = None,
groups = 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 ' print('error: Cannot remove project "%s": uncommitted changes '
'are present' % project.relpath, file=sys.stderr) 'are present' % project.relpath, file=sys.stderr)
print(' commit changes, then run sync again', print(' commit changes, then run sync again',
file=sys.stderr) file=sys.stderr)
return -1 return 1
else: elif self._DeleteProject(project.worktree):
print('Deleting obsolete path %s' % project.worktree, return 1
file=sys.stderr)
shutil.rmtree(project.worktree)
# Try deleting parent subdirs if they are empty
project_dir = os.path.dirname(project.worktree)
while project_dir != self.manifest.topdir:
try:
os.rmdir(project_dir)
except OSError:
break
project_dir = os.path.dirname(project_dir)
new_project_paths.sort() new_project_paths.sort()
fd = open(file_path, 'w') fd = open(file_path, 'w')
@ -516,140 +739,141 @@ later is required to fix a server side protocol bug.
fd.close() fd.close()
return 0 return 0
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)
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 = os.path.basename(smart_sync_manifest_path)
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)
return manifest_name
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): def Execute(self, opt, args):
if opt.jobs: if opt.jobs:
self.jobs = opt.jobs self.jobs = opt.jobs
if self.jobs > 1: if self.jobs > 1:
soft_limit, _ = _rlimit_nofile() soft_limit, _ = _rlimit_nofile()
self.jobs = min(self.jobs, (soft_limit - 5) / 3) 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)
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: if opt.manifest_name:
self.manifest.Override(opt.manifest_name) self.manifest.Override(opt.manifest_name)
manifest_name = opt.manifest_name manifest_name = opt.manifest_name
smart_sync_manifest_name = "smart_sync_override.xml"
smart_sync_manifest_path = os.path.join( smart_sync_manifest_path = os.path.join(
self.manifest.manifestProject.worktree, smart_sync_manifest_name) self.manifest.manifestProject.worktree, 'smart_sync_override.xml')
if opt.smart_sync or opt.smart_tag: if opt.smart_sync or opt.smart_tag:
if not self.manifest.manifest_server: manifest_name = self._SmartSyncSetup(opt, smart_sync_manifest_path)
print('error: cannot smart sync: no manifest server defined in ' else:
'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 os.path.isfile(smart_sync_manifest_path):
try: try:
os.remove(smart_sync_manifest_path) platform_utils.remove(smart_sync_manifest_path)
except OSError as e: except OSError as e:
print('error: failed to remove existing smart sync override manifest: %s' % print('error: failed to remove existing smart sync override manifest: %s' %
e, file=sys.stderr) e, file=sys.stderr)
@ -664,15 +888,25 @@ later is required to fix a server side protocol bug.
_PostRepoUpgrade(self.manifest, quiet=opt.quiet) _PostRepoUpgrade(self.manifest, quiet=opt.quiet)
if not opt.local_only: if not opt.local_only:
mp.Sync_NetworkHalf(quiet=opt.quiet, start = time.time()
current_branch_only=opt.current_branch_only, success = mp.Sync_NetworkHalf(quiet=opt.quiet,
no_tags=opt.no_tags, current_branch_only=opt.current_branch_only,
optimized_fetch=opt.optimized_fetch) no_tags=opt.no_tags,
optimized_fetch=opt.optimized_fetch,
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)
if mp.HasChanges: if mp.HasChanges:
syncbuf = SyncBuffer(mp.config) syncbuf = SyncBuffer(mp.config)
mp.Sync_LocalHalf(syncbuf) start = time.time()
if not syncbuf.Finish(): mp.Sync_LocalHalf(syncbuf, submodules=self.manifest.HasSubmodules)
clean = syncbuf.Finish()
self.event_log.AddSync(mp, event_log.TASK_SYNC_LOCAL,
start, time.time(), clean)
if not clean:
sys.exit(1) sys.exit(1)
self._ReloadManifest(manifest_name) self._ReloadManifest(manifest_name)
if opt.jobs is None: if opt.jobs is None:
@ -709,8 +943,8 @@ later is required to fix a server side protocol bug.
# generate a new args list to represent the opened projects. # generate a new args list to represent the opened projects.
# TODO: make this more reliable -- if there's a project name/path overlap, # TODO: make this more reliable -- if there's a project name/path overlap,
# this may choose the wrong project. # this may choose the wrong project.
args = [os.path.relpath(self.manifest.paths[p].worktree, os.getcwd()) args = [os.path.relpath(self.manifest.paths[path].worktree, os.getcwd())
for p in opened_projects] for path in opened_projects]
if not args: if not args:
return return
all_projects = self.GetProjects(args, all_projects = self.GetProjects(args,
@ -757,20 +991,10 @@ later is required to fix a server side protocol bug.
# bail out now, we have no working tree # bail out now, we have no working tree
return return
if self.UpdateProjectList(): if self.UpdateProjectList(opt):
sys.exit(1) sys.exit(1)
syncbuf = SyncBuffer(mp.config, self._Checkout(all_projects, opt)
detach_head = opt.detach_head)
pm = Progress('Syncing work tree', len(all_projects))
for project in all_projects:
pm.update()
if project.worktree:
project.Sync_LocalHalf(syncbuf, force_sync=opt.force_sync)
pm.end()
print(file=sys.stderr)
if not syncbuf.Finish():
sys.exit(1)
# If there's a notice that's supposed to print at the end of the sync, print # If there's a notice that's supposed to print at the end of the sync, print
# it now... # it now...
@ -850,6 +1074,7 @@ def _VerifyTag(project):
return False return False
return True return True
class _FetchTimes(object): class _FetchTimes(object):
_ALPHA = 0.5 _ALPHA = 0.5
@ -880,7 +1105,7 @@ class _FetchTimes(object):
f.close() f.close()
except (IOError, ValueError): except (IOError, ValueError):
try: try:
os.remove(self._path) platform_utils.remove(self._path)
except OSError: except OSError:
pass pass
self._times = {} self._times = {}
@ -904,7 +1129,7 @@ class _FetchTimes(object):
f.close() f.close()
except (IOError, TypeError): except (IOError, TypeError):
try: try:
os.remove(self._path) platform_utils.remove(self._path)
except OSError: except OSError:
pass pass

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2008 The Android Open Source Project # Copyright (C) 2008 The Android Open Source Project
# #
@ -25,12 +26,10 @@ from git_command import GitCommand
from project import RepoHook from project import RepoHook
from pyversion import is_python3 from pyversion import is_python3
# pylint:disable=W0622
if not is_python3(): if not is_python3():
input = raw_input input = raw_input
else: else:
unicode = str unicode = str
# pylint:enable=W0622
UNUSUAL_COMMIT_THRESHOLD = 5 UNUSUAL_COMMIT_THRESHOLD = 5
@ -80,8 +79,7 @@ added to the respective list of users, and emails are sent to any
new users. Users passed as --reviewers must already be registered new users. Users passed as --reviewers must already be registered
with the code review system, or the upload will fail. with the code review system, or the upload will fail.
Configuration # Configuration
-------------
review.URL.autoupload: review.URL.autoupload:
@ -128,10 +126,9 @@ is set to "true" then repo will assume you always want the equivalent
of the -t option to the repo command. If unset or set to "false" then of the -t option to the repo command. If unset or set to "false" then
repo will make use of only the command line option. repo will make use of only the command line option.
References # References
----------
Gerrit Code Review: http://code.google.com/p/gerrit/ Gerrit Code Review: https://www.gerritcodereview.com/
""" """
@ -154,6 +151,19 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
p.add_option('-d', '--draft', p.add_option('-d', '--draft',
action='store_true', dest='draft', default=False, action='store_true', dest='draft', default=False,
help='If specified, upload as a draft.') help='If specified, upload as a draft.')
p.add_option('--ne', '--no-emails',
action='store_false', dest='notify', default=True,
help='If specified, do not send emails on upload.')
p.add_option('-p', '--private',
action='store_true', dest='private', default=False,
help='If specified, upload as a private change.')
p.add_option('-w', '--wip',
action='store_true', dest='wip', default=False,
help='If specified, upload as a work-in-progress change.')
p.add_option('-o', '--push-option',
type='string', action='append', dest='push_options',
default=[],
help='Additional push options to transmit')
p.add_option('-D', '--destination', '--dest', p.add_option('-D', '--destination', '--dest',
type='string', action='store', dest='dest_branch', type='string', action='store', dest='dest_branch',
metavar='BRANCH', metavar='BRANCH',
@ -175,6 +185,9 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
# Never run upload hooks, but upload anyway (AKA bypass hooks). # Never run upload hooks, but upload anyway (AKA bypass hooks).
# - no-verify=True, verify=True: # - no-verify=True, verify=True:
# Invalid # Invalid
p.add_option('--no-cert-checks',
dest='validate_certs', action='store_false', default=True,
help='Disable verifying ssl certs (unsafe).')
p.add_option('--no-verify', p.add_option('--no-verify',
dest='bypass_hooks', action='store_true', dest='bypass_hooks', action='store_true',
help='Do not run the upload hook.') help='Do not run the upload hook.')
@ -198,7 +211,8 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
commit_list = branch.commits commit_list = branch.commits
destination = opt.dest_branch or project.dest_branch or project.revisionExpr destination = opt.dest_branch or project.dest_branch or project.revisionExpr
print('Upload project %s/ to remote branch %s:' % (project.relpath, destination)) print('Upload project %s/ to remote branch %s%s:' %
(project.relpath, destination, ' (draft)' if opt.draft else ''))
print(' branch %s (%2d commit%s, %s):' % ( print(' branch %s (%2d commit%s, %s):' % (
name, name,
len(commit_list), len(commit_list),
@ -207,7 +221,9 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
for commit in commit_list: for commit in commit_list:
print(' %s' % commit) 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 = sys.stdin.readline().strip().lower()
answer = answer in ('y', 'yes', '1', 'true', 't') answer = answer in ('y', 'yes', '1', 'true', 't')
@ -346,10 +362,13 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
# if they want to auto upload, let's not ask because it could be automated # if they want to auto upload, let's not ask because it could be automated
if answer is None: if answer is None:
sys.stdout.write('Uncommitted changes in ' + branch.project.name) print()
sys.stdout.write(' (did you forget to amend?):\n') print('Uncommitted changes in %s (did you forget to amend?):'
sys.stdout.write('\n'.join(changes) + '\n') % branch.project.name)
sys.stdout.write('Continue uploading? (y/N) ') 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() a = sys.stdin.readline().strip().lower()
if a not in ('y', 'yes', 't', 'true', 'on'): if a not in ('y', 'yes', 't', 'true', 'on'):
print("skipping upload", file=sys.stderr) print("skipping upload", file=sys.stderr)
@ -377,7 +396,16 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
branch.uploaded = False branch.uploaded = False
continue continue
branch.UploadForReview(people, auto_topic=opt.auto_topic, draft=opt.draft, dest_branch=destination) branch.UploadForReview(people,
auto_topic=opt.auto_topic,
draft=opt.draft,
private=opt.private,
notify=None if opt.notify else 'NONE',
wip=opt.wip,
dest_branch=destination,
validate_certs=opt.validate_certs,
push_options=opt.push_options)
branch.uploaded = True branch.uploaded = True
except UploadError as e: except UploadError as e:
branch.error = e branch.error = e
@ -454,11 +482,17 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
if avail: if avail:
pending.append((project, avail)) pending.append((project, avail))
if pending and (not opt.bypass_hooks): if not pending:
print("no branches ready for upload", file=sys.stderr)
return
if not opt.bypass_hooks:
hook = RepoHook('pre-upload', self.manifest.repo_hooks_project, hook = RepoHook('pre-upload', self.manifest.repo_hooks_project,
self.manifest.topdir, abort_if_user_denies=True) self.manifest.topdir,
pending_proj_names = [project.name for (project, avail) in pending] self.manifest.manifestProject.GetRemote('origin').url,
pending_worktrees = [project.worktree for (project, avail) in pending] abort_if_user_denies=True)
pending_proj_names = [project.name for (project, available) in pending]
pending_worktrees = [project.worktree for (project, available) in pending]
try: try:
hook.Run(opt.allow_all_hooks, project_list=pending_proj_names, hook.Run(opt.allow_all_hooks, project_list=pending_proj_names,
worktree_list=pending_worktrees) worktree_list=pending_worktrees)
@ -472,9 +506,7 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
cc = _SplitEmails(opt.cc) cc = _SplitEmails(opt.cc)
people = (reviewers, cc) people = (reviewers, cc)
if not pending: if len(pending) == 1 and len(pending[0][1]) == 1:
print("no branches ready for upload", file=sys.stderr)
elif len(pending) == 1 and len(pending[0][1]) == 1:
self._SingleBranch(opt, pending[0][1][0], people) self._SingleBranch(opt, pending[0][1][0], people)
else: else:
self._MultipleBranches(opt, pending, people) self._MultipleBranches(opt, pending, people)

View File

@ -1,3 +1,4 @@
# -*- coding:utf-8 -*-
# #
# Copyright (C) 2009 The Android Open Source Project # Copyright (C) 2009 The Android Open Source Project
# #
@ -40,5 +41,5 @@ class Version(Command, MirrorSafeCommand):
print('repo launcher version %s' % Version.wrapper_version) print('repo launcher version %s' % Version.wrapper_version)
print(' (from %s)' % Version.wrapper_path) print(' (from %s)' % Version.wrapper_path)
print(git.version().strip()) print('git %s' % git.version_tuple().full)
print('Python %s' % sys.version) 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

49
tests/test_git_command.py Normal file
View File

@ -0,0 +1,49 @@
# -*- 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 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 = (1, 7, 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)

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 os
import unittest import unittest

62
tests/test_project.py Normal file
View File

@ -0,0 +1,62 @@
# -*- 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 unittest
import project
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)

View File

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

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python # -*- coding:utf-8 -*-
# #
# Copyright (C) 2014 The Android Open Source Project # Copyright (C) 2014 The Android Open Source Project
# #
@ -15,7 +15,12 @@
# limitations under the License. # limitations under the License.
from __future__ import print_function 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 import os
@ -26,5 +31,5 @@ _wrapper_module = None
def Wrapper(): def Wrapper():
global _wrapper_module global _wrapper_module
if not _wrapper_module: if not _wrapper_module:
_wrapper_module = imp.load_source('wrapper', WrapperPath()) _wrapper_module = _loader('wrapper', WrapperPath())
return _wrapper_module return _wrapper_module