Compare commits

...

75 Commits

Author SHA1 Message Date
6392c87945 sync: Allow -j to have a default in manifest
This permits manifest authors to suggest a number of parallel
fetch operations against a remote server. For example, Gerrit
Code Review servers support queuing of requests and processes
them in first-in, first-out order. Running concurrent fetches
can utilize multiple CPUs on the Gerrit server, but will also
decrease overall operation latency by having the request put
into the queue ready to execute as soon as a CPU is free.

Change-Id: I3d3904acb6f63516bae4b071c510ad57a2afab18
Signed-off-by: Shawn O. Pearce <sop@google.com>
2011-09-22 18:08:27 -07:00
97d2b2f7a0 sync: Limit -j to file descriptors
Each worker thread requires at least 3 file descriptors to run the
forked 'git fetch' child to operate against the local repository.
Mac OS X has the RLIMIT_NOFILE set to 256 by default, which means
a sync -j128 often fails when the workers run out of pipes within
the Python parent process.

Change-Id: I2cdb14621b899424b079daf7969bc8c16b85b903
Signed-off-by: Shawn O. Pearce <sop@google.com>
2011-09-22 18:08:26 -07:00
3a0e782790 Add global option --time to track execution
This prints a simple line after a command ends, providing
information about how long it executed for using real wall
clock time. Its mostly useful for looking at sync times.

Change-Id: Ie0997df0a0f90150270835d94b58a01a10bc3956
Signed-off-by: Shawn O. Pearce <sop@google.com>
2011-09-22 18:08:18 -07:00
490d09a314 Support units in progress messages
This allows our progress meter to be used for bytes transferred, by
setting the units to KB or MB to let the user know the size.

Change-Id: Ie8653d4a40d79439026c18bd51915845b2c5bba9
Signed-off-by: Shawn O. Pearce <sop@google.com>
2011-09-19 14:52:57 -07:00
13111b4e97 Add support for url.*.insteadof
Teach repo how to resolve URLs using the url.insteadof feature
that C Git natively uses during clone, fetch or push. This will
later allow repo to resolve a URL before accessing it directly.
We do not want to pre-resolve things and store the resolved URL
into individual projects, as this makes it impossible for the
user to undo the insteadof mapping at a later date.

Change-Id: I0f62e811197c53fbc8a8be424e3cabf4ed07b4cb
Signed-off-by: Shawn O. Pearce <sop@google.com>
2011-09-19 14:52:57 -07:00
bd0312a484 Support ~/.netrc for HTTP Basic authentication
If repo tries to access a URL over HTTP and the user needs to
authenticate, offer a match from ~/.netrc. This matches behavior
with the Git command line client.

Change-Id: I803f3c5d562177ea0330941350cff3cc1e1bef08
Signed-off-by: Shawn O. Pearce <sop@google.com>
2011-09-19 14:52:32 -07:00
334851e4b6 Enhance HTTP support
Setting REPO_CURL_VERBOSE=1 in the environment will register a debug
level HTTPHandler on the urllib2 library, showing HTTP requests and
responses on the stderr channel of repo.

During any HTTP or HTTPS request created inside of the repo process,
a custom User-Agent header is now defined:

  User-Agent: git-repo/1.7.5 (Linux) git/1.7.7 Python/2.6.5

Change-Id: Ia5026fb1e1500659bd2af27416d85e205048bf26
Signed-off-by: Shawn O. Pearce <sop@google.com>
2011-09-19 14:51:47 -07:00
014d060989 Honor http_proxy variable globally
If the http_proxy environment variable was set, honor it during
the entire repo session for any Python created HTTP connections.

Change-Id: Ib4ae833cb2cdd47ab0126949f6b399d2c142887d
Signed-off-by: Shawn O. Pearce <sop@google.com>
2011-09-11 13:11:04 -07:00
44da16e8a0 Change default REPO_URL to code.google.com
Change-Id: If7700daf96fb8f3ee449e5774017272ef31b4b44
2011-09-05 14:16:49 -07:00
65e0f35fda Add commit-msg hook also for manifest project
The manifest project has - by design - not a review URL associated
with it. It is actually not even a 'project' in repo's sense.

This will prevent the commit-msg hook from being added, which is
not necessarily wanted as the project is managed in gerrit.

This commit will enable the commit-msg hook, which in turn will
add the Change-Id-line to every new commit in it. This simplifies
replacing patch sets (by git push ... refs/for/...).

Change-Id: I42d0f6fd79e6282d9d47074a3819e68d968999a7
Signed-off-by: Victor Boivie <victor.boivie@sonyericsson.com>
2011-07-20 07:34:23 -07:00
08c880db18 Smart tag support
This is an evolution of 'smart-sync' that adds a new option, -t,
that allows you to specify a tag/label to use instead of the
"latest good build" on the current manifest branch which -s does.

Signed-off-by: Victor Boivie <victor.boivie@sonyericsson.com>
Change-Id: I8c20fd91104a6aafa0271d4d33f6c4850aade17e
2011-07-20 07:13:48 -07:00
a101f1c167 Honor 'http_proxy' environment variable
'repo upload' makes http request using urllib2 python library.
Unfortunately this library does not work (by default) in case
if the user behind a proxy.

This change adds proxy handler in case if 'http_proxy' environment
variable is set.

Change-Id: Ic4176ad733fc21bd5b59661b3eacc2f0a7c3c1ff
2011-07-20 07:11:00 -07:00
49cd59bc86 Add --depth option to main repo wrapper.
See related repo change:
  https://review.source.android.com/#change,22722

Change-Id: I9bdd86971c94604477b91cdf47d6fac2c0bc186e
2011-06-14 16:59:19 -07:00
30d452905f Add a --depth option to repo init.
Change-Id: Id30fb4a85f4f8a1847420b0b51a86060041eb5bf
2011-06-09 16:48:23 -07:00
d6c93a28ca Add branch support to repo upload
This commit adds a --br=<branch> option to repo upload.

repo currently examines every non-published branch. This is problematic
for my workflow. I have many branches in my kernel tree. Many of these
branches are based off of upstream remotes (I have many remotes) and
will never be uploaded (they'll get sent upstream as a patch).

Having repo scan these branches adds to my upload processing time
and clutters the branch selection buffer. I've also seen repo get
confused when one of my branches is 1000s of commits different from
m/master.

Change-Id: I68fa18951ea59ba373277b57ffcaf8cddd7e7a40
2011-05-26 10:49:39 -07:00
d572a13021 Added repo cherry-pick command
It is undesired to have the same Change-Id:-line for two separate
commits, and when cherry-picking, the user must manually change it.

If this is not done, bad things may happen (such as when the user
is uploading the cherry-picked commit to Gerrit, it will instead
see it as a new patch-set for the original change, or worse).

repo cherry-pick works the same was as git cherry-pick, except that
it replaces the Change-Id with a new one and adds a reference
back to the commit from where it was picked.

On failures (when git can not successfully apply the cherry-picked
commit), instructions will be written to the user.

Change-Id: I5a38b89839f91848fad43386d43cae2f6cdabf83
2011-04-07 17:19:06 -04:00
3ba5f95b46 Fixed repo checkout error message when git reports errors.
In the current version of repo checkout, we often get the error:
  error: no project has branch xyzzy

...even when the actual error was something else.  This fixes it
to only report the 'no project has branch' when that is actually true.

This fix is very similar to one made for 'repo abandon':
  https://review.source.android.com/#change,22207

The repo checkout error is filed as: <http://crosbug.com/6514>

TEST=manual

A sample creating a case where 'git checkout' will fail:

  $ repo start branch1 .
  $ repo start branch2 .
  $ touch bogusfile
  $ git add bogusfile
  $ git commit -m "create bogus file"
  [branch2 f8b6b08] create bogus file
   0 files changed, 0 insertions(+), 0 deletions(-)
   create mode 100644 bogusfile
  $ echo "More" >> bogusfile
  $ repo checkout branch1 .
  error: chromite/: cannot checkout branch1

A sample case showing that we still fail if no project has a branch:

  $ repo checkout xyzzy .
  error: no project has branch xyzzy

Change-Id: I48a8e258fa7a9c1f2800dafc683787204bbfcc63
2011-04-07 16:55:35 -04:00
2630dd9787 Fixed problems w/ 2nd repo init if first repo init had bad URL.
This is the simplest fix: if we had problems syncing the
manifest.git directory and we were the ones that created it,
we should delete it.  This doesn't try to do anything complex
like try to recover from a .repo directory that got broken in
some other way.

This is filed as: <http://crosbug.com/13403>

TEST=manual

Init once with a bad URL:
  $ repo init -u http://foobar.example.com
  Getting manifest ...
     from http://foobar.example.com
  Connection closed by 172.22.121.77
  error: Couldn't resolve host 'foobar.example.com' while accessing http://foobar.example.com/info/refs

  fatal: HTTP request failed
  fatal: cannot obtain manifest http://foobar.example.com

Init again: identical to the first.  Good:
  $ repo init -u http://foobar.example.com
  Getting manifest ...
     from http://foobar.example.com
  Connection closed by 172.22.121.77
  error: Couldn't resolve host 'foobar.example.com' while accessing http://foobar.example.com/info/refs

  fatal: HTTP request failed
  fatal: cannot obtain manifest http://foobar.example.com

Init with correct URL:
  $ repo init -u http://git.chromium.org/git/manifest -m minilayout.xml
  Getting manifest ...
     from http://git.chromium.org/git/manifest
  [ ... cut ... ]

  repo initialized in /.../repoiniterr

Try a bad URL after a good one; it doesn't get saved (good):
  $ repo init -u http://foobar.example.com
  Connection closed by 172.22.121.77
  error: Couldn't resolve host 'foobar.example.com' while accessing http://foobar.example.com/info/refs

  fatal: HTTP request failed
  fatal: cannot obtain manifest http://foobar.example.com

Just to confirm, I can still do a good one after a bad...
  $ repo init -u http://git.chromium.org/git/manifest -m minilayout.xml

  Your Name  [George Washington]:
  Your Email [george@washington.example.com]:

  Your identity is: George Washington <george@washington.example.com>
  is this correct [y/n]? y

  repo initialized in /.../repoiniterr

Change-Id: I1692821a330d97b1d218b2e191a93245b33f2362
2011-04-07 16:51:50 -04:00
dafb1d68d3 Fixed repo abandon to give better messages.
The main fix is to give an error message if nothing was actually
abandoned.  See <http://crosbug.com/6041>.

The secondary fix is to list projects where the abandon happened.
This could be done in a separate CL or dropped altogether if requested.

TEST=manual

$ repo abandon dougabc; echo $?
Abandon dougabc: 100% (127/127), done.
Abandoned in 2 project(s):
  chromite
  src/platform/init
0

$ repo abandon dougabc; echo $?
Abandon dougabc: 100% (127/127), done.
error: no project has branch dougabc
1

$ repo abandon dougabc; echo $?
Abandon dougabc: 100% (127/127), done.
error: chromite/: cannot abandon dougabc
1

Change-Id: I79520cc3279291acadc1a24ca34a761e9de04ed4
2011-04-07 16:49:23 -04:00
4655e81a75 Add option to check status of projects in parallel.
Change-Id: I6ac653f88573def8bb3d96031d3570ff966251ad
2011-04-07 16:36:42 -04:00
723c5dc3d6 Fix parallel sync on python < 2.6.
Event.isSet was renamed to is_set in 2.6, but we should
use the earlier syntax to avoid breaking compatibility
with older Python installations.

Change-Id: I41888ed38df278191d7496c1a6eed15e881733f4
2011-04-04 11:34:47 -04:00
e6a0eeb80d sync: Fix syntax error on Python 2.4
Change-Id: I371d032d5a1ddde137721cbe2b24bfa38f20aaaa
Signed-off-by: Shawn O. Pearce <sop@google.com>
2011-03-22 19:04:47 -07:00
0960b5b53d Creating rr-cache
If git-rerere is enabled, it uses the rr-cache directory that
repo currently creates a symlink from, but doesn't create the
destination directory (inside the project's directory). Git
will then complain during merges and rebases.

This commit creates the rr-cache directory inside the project.

Change-Id: If8b57a04f022fc6ed6a7007d05aa2e876e6611ee
2011-03-17 09:19:51 -07:00
fc06ced9f9 Make 'repo sync -jN' exit with an error code in the case of sync errors.
The bug that this is fixing is described here:

http://code.google.com/p/chromium-os/issues/detail?id=6813

This fix allows the helper threads to signal the main thread that they
saw an error.  When the main thread sees the error, it will let all
existing threads finish, then exit with an error.

Change-Id: If3019bc6b0b3ab9304d49ed2eea53e9d57f3095a
2011-03-17 09:17:42 -07:00
fce89f218a Add 'list' command to repo.
This isn't a required command, but might be more discoverable for
repo newbies?

Change-Id: If357346f234774d42e04e024e65acdaf6dca6c62
2011-03-16 12:55:44 -07:00
37282b4b9c Support repo-level pre-upload hook and prep for future hooks.
All repo-level hooks are expected to live in a single project at the
top level of that project.  The name of the hooks project is provided
in the manifest.xml.  The manifest also lists which hooks are enabled
to make it obvious if a file somehow failed to sync down (or got
deleted).

Before running any hook, we will prompt the user to make sure that it
is OK.  A user can deny running the hook, allow once, or allow
"forever" (until hooks change).  This tries to keep with the git
spirit of not automatically running anything on the user's computer
that got synced down.  Note that individual repo commands can add
always options to avoid these prompts as they see fit (see below for
the 'upload' options).

When hooks are run, they are loaded into the current interpreter (the
one running repo) and their main() function is run.  This mechanism is
used (instead of using subprocess) to make it easier to expand to a
richer hook interface in the future.  During loading, the
interpreter's sys.path is updated to contain the directory containing
the hooks so that hooks can be split into multiple files.

The upload command has two options that control hook behavior:
  - no-verify=False, verify=False (DEFAULT):
    If stdout is a tty, can prompt about running upload hooks if needed.
    If user denies running hooks, the upload is cancelled.  If stdout is
    not a tty and we would need to prompt about upload hooks, upload is
    cancelled.
  - no-verify=False, verify=True:
    Always run upload hooks with no prompt.
  - no-verify=True, verify=False:
    Never run upload hooks, but upload anyway (AKA bypass hooks).
  - no-verify=True, verify=True:
    Invalid

Sample bit of manifest.xml code for enabling hooks (assumes you have a
project named 'hooks' where hooks are stored):
  <repo-hooks in-project="hooks" enabled-list="pre-upload" />

Sample main() function in pre-upload.py in hooks directory:
  def main(project_list, **kwargs):
    print ('These projects will be uploaded: %s' %
           ', '.join(project_list))
    print ('I am being a good boy and ignoring anything in kwargs\n'
           'that I don\'t understand.')
    print 'I fail 50% of the time.  How flaky.'
    if random.random() <= .5:
      raise Exception('Pre-upload hook failed.  Have a nice day.')

Change-Id: I5cefa2cd5865c72589263cf8e2f152a43c122f70
2011-03-11 11:53:23 -08:00
835cd6888f Post-nonexistent-revision crash sidestepped
Fix for the bug that leaves a fractional .git directory after attempting to
perform an initial sync to a nonexistent revision. Moved the initialization of
the working directory to after the revision ID has already been checked. Now,
no project/.git directory gets created at all if the revision ID is bad.

Change-Id: I0c9b2a59573410f1d11de7661591bf02e4ce326b
2011-03-08 13:48:24 -08:00
8ced8641c8 Renamed 'repo_hooks' function to '_ProjectHooks'.
This renaming was done for two reasons:
1. The hooks are actually project-level hooks, not repo-level
   hooks.  Since we are talking about adding repo-level hooks,
   It keeps things less confusing if we name the existing hooks
   to be "ProjectHooks"
2. The function is a private function in project.py and so
   should have capitalization to match.

I also added a docstring describing this function.

Change-Id: I1d30f5de08e8f9f99f78146e68c76f906782d97e
2011-02-01 09:57:29 -08:00
2536f80625 Fixed bug identifying 'commit-msg' files.
There was a minor typo that would cause repo to (I believe)
mistakenly identify any file that contained a substring of the
word 'commit-msg' as a commit message hook.  For example, the file
'mit' or the file 'msg' would be treated as a commit message hook.
I believe that it was intended that repo only recognize files
named exactly 'commit-msg'.

Change-Id: I93edbddf3da3cf0935641e6efb19b0a8ee6e2308
2011-02-01 09:53:56 -08:00
0ce6ca9c7b Fix mirror clients with no worktree
Commit "Make path references OS independent" (df14a70c45)
broke mirror clients by trying to invoke replace() on None
when there is no worktree.

Change-Id: Ie0a187058358f7dcdf83119e45cc65409c980f11
2011-01-10 13:26:34 -08:00
0fc3a39829 Bump repo version to 1,10
Change-Id: Ifdc041e7152af31de413b9269f20000acd945b3b
2011-01-10 09:01:24 -08:00
c7c57e34db help: Don't show empty Summary or Description sections
Signed-off-by: Shawn O. Pearce <sop@google.com>
(cherry picked from commit 60e679209a)
2011-01-09 17:39:22 -08:00
0d2b61f11d sync: Run git gc --auto after fetch
Users may wind up with a lot of loose object content in projects they
don't frequently make changes in, but that are modified by others.

Since we bypass many git code paths that would have otherwise called
out to `git gc --auto`, its possible for these projects to have
their loose object database grow out of control.  To help prevent
that, we now invoke it ourselves during the network half of sync.

Signed-off-by: Shawn O. Pearce <sop@google.com>
(cherry picked from commit 1875ddd47c)
2011-01-09 17:39:22 -08:00
2bf9db0d3b Add "repo branch" as an alias for "repo branches"
For those of us that are used to typing "git branch".

Signed-off-by: Mike Lockwood <lockwood@android.com>
(cherry picked from commit 33f0e786bb)
2011-01-09 17:39:22 -08:00
f00e0ce556 upload: Catch and cleanly report connectivity errors
Instead of giving a Python backtrace when there is a connectivity
problem during repo upload, report that we cannot access the host,
and why, with a halfway decent error message.

Bug: REPO-45
Change-Id: I9a45b387e86e48073a2d99bd6d594c1a7d6d99d4
Signed-off-by: Shawn O. Pearce <sop@google.com>
(cherry picked from commit d2dfac81ad)
2011-01-09 17:39:22 -08:00
1b5a4a0c5d forall: Silently skip missing projects
If a project is missing locally, it might be OK to skip over it
and continue running the same command in other projects.

Bug: REPO-43
Change-Id: I64f97eb315f379ab2c51fc53d24ed340b3d09250
Signed-off-by: Shawn O. Pearce <sop@google.com>
(cherry picked from commit d4cd69bdef)
2011-01-09 17:39:22 -08:00
de8b2c4276 Fix to display the usage message of the command download when the user
don't provide any arguments to 'repo download'.

Signed-off-by: Thiago Farina <thiago.farina@gmail.com>
(cherry picked from commit 840ed0fab7)
2011-01-09 17:39:22 -08:00
727ee98a40 Use os.environ.copy() instead of dict()
Signed-off-by: Shawn O. Pearce <sop@google.com>
(cherry picked from commit 3218c13205)
2011-01-09 17:39:22 -08:00
df14a70c45 Make path references OS independent
Change-Id: I5573995adfd52fd54bddc62d1d1ea78fb1328130
(cherry picked from commit b0f9a02394)

Conflicts:

	command.py
2011-01-09 17:39:19 -08:00
f18cb76173 Encode the environment variables passed to git
Windows allows the environment to have unicode values.
This will cause Python to fail to execute the command.

Change-Id: I37d922c3d7ced0d5b4883f0220346ac42defc5e9
Signed-off-by: Shawn O. Pearce <sop@google.com>
2011-01-09 16:13:56 -08:00
d3fd537ea5 Exit with statuscode 0 for repo help init
The complete help text is printed, so the program executed successfully.

Some tools (like OpenGrok) detects the availibility of a program by
running it with a known set of options and check the return code.
It is an easy and portable way of checking for the existence of a program
instead of searching the path (and handle extensions) ourselves.

Change-Id: Ic13428c77be4a36d599ccb8c86d893308818eae3
2011-01-09 16:10:04 -08:00
0048b69c03 Fixed race condition in 'repo sync -jN' that would open multiple masters.
This fixes the SSH Control Masters to be managed in a thread-safe
fashion.  This is important because "repo sync -jN" uses threads to
sync more than one repository at the same time.  The problem didn't
show up earlier because it was masked if all of the threads tried to
connect to the same host that was used on the "repo init" line.
2010-12-21 13:39:23 -08:00
2b8db3ce3e Added feature to print a <notice> from manifest at the end of a sync.
This feature is used to convey information on a when a branch has
ceased development or if it is an experimental branch with a few
gotchas, etc.

You add it to your manifest XML by doing something like this:
<manifest>
  <notice>
    NOTE TO DEVELOPERS:
      If you checkin code, you have to pinky-swear that it contains no bugs.
      Anyone who breaks their promise will have tomatoes thrown at them in the
      team meeting.  Be sure to bring an extra set of clothes.
  </notice>

  <remote ... />
  ...
</manifest>

Carriage returns and indentation are relevant for the text in this tag.

This feature was requested by Anush Elangovan on the ChromiumOS team.
2010-11-01 15:08:06 -07:00
5df6de075e sync: Use --force-broken to continue other projects
This adds a new flag -f/--force-broken that will allow the rest of
the sync process to continue instead of bailing when a particular
project fails to sync.

Change-Id: I23680f2ee7927410f7ed930b1d469424c9aa246e
Signed-off-by: Andrei Warkentin <andreiw@motorola.com>
Signed-off-by: Shawn O. Pearce <sop@google.com>
2010-10-29 12:20:01 -07:00
a0de6e8eab upload: Remove --replace option
It hasn't been necessary for a long time, and its
functionality can be accomplished with 'git push'.

Change-Id: Ic00d3adbe4cee7be3955117489c69d6e90106559
2010-10-29 12:12:56 -07:00
16614f86b3 sync --quiet: be more quiet
Change-Id: I5e8363c7b32e4546d1236cfc5a32e01c3e5ea8e6
Signed-off-by: Shawn O. Pearce <sop@google.com>
2010-10-29 12:08:57 -07:00
88443387b1 sync: Enable use of git clone --reference
Use git clone to initialize a new repository, and when possible
allow callers to use --reference to reuse an existing checkout as
the initial object storage area for the new checkout.

Change-Id: Ie27f760247f311ce484c6d3e85a90d94da2febfc
Signed-off-by: Shawn O. Pearce <sop@google.com>
2010-10-29 12:08:50 -07:00
99482ae58a Only delete corrupt pickle config files if they exist
os.remove() raises OSError if the file being removed doesn't exist.
Check before calling to ensure we don't raise a useless exception
on an already deleted file.

Change-Id: I44c1c7dd97a47fcab8afb6c18fdf179158b6dab7
Signed-off-by: Shawn O. Pearce <sop@google.com>
2010-10-29 08:25:04 -07:00
ec1df9b7f6 Don't allow git fetch to start ControlMaster
To avoid connectivity problems, we don't want the ssh process
that is started by git fetch to become a ControlMaster for the
overall sync task.  If it did, we would lose connectivity when
git fetch was finished with the current project, causing later
projects to not fetch efficiently.

Change-Id: I8d0dcf9b361276ff8c8b5a6324cbd4a501e9c4dd
Signed-off-by: Shawn O. Pearce <sop@google.com>
2010-10-29 08:15:14 -07:00
06d029c1c8 Check for existing SSH ControlMaster
Be more thorough about checking for an existing ssh master by
running a test command first, and only opening up a new master
if the test fails to connect.

Change-Id: I56fe8e7b4dbc123675b7f259e81d359ed0cd55cf
Signed-off-by: Shawn O. Pearce <sop@google.com>
2010-10-29 08:14:56 -07:00
b715b14807 Fix for handling values of EDITOR which contain a space.
The shell swallows the 0th arg, which was the filename. Simple fix
is to pass in an extra arg for the shell to swallow.

Change-Id: Iad6304ba9ccea6e7262ee06ef87d3dac57dbde81
2010-08-06 17:05:04 -07:00
60829ba72f upload: Fix --replace flag
--replace started to fail due to a Python error, I forgot to pass
through the opt structure to the replace function.

Change-Id: Ifcd7a0c715c3fd9070a4c58208612a626382de35
Signed-off-by: Shawn O. Pearce <sop@google.com>
2010-07-16 07:42:45 -07:00
a22f99ae41 rebase: Pass through more options
Passing through --whitespace=fix to rebase can be useful
to clean up a branch prior to uploading it for review.

Change-Id: Id85f1912e5e11ff9602e3b342c2fd7441abe67d7
Signed-off-by: Shawn O. Pearce <sop@google.com>
2010-07-15 17:43:02 -07:00
3575b8f8bd upload: Allow review.HOST.username to override email
Some users might need to use a different login name than the local
part of their email address for their Gerrit Code Review user
account.  Allow it to be overridden with the review.HOST.username
configuration variable.

Change-Id: I714469142ac7feadf09fee9c26680c0e09076b75
Signed-off-by: Shawn O. Pearce <sop@google.com>
2010-07-15 17:03:19 -07:00
a5ece0e050 upload -t: Automatically include local branch name
If the -t flag is given to upload, the local branch name is
automatically sent to Gerrit Code Review as the topic branch name
for the change(s).  This requires the server to be Gerrit Code
Review v2.1.3-53-gd50c94e or later, which isn't widely deployed
right now, so the default is opt-out.

Change-Id: I034fcacb405b7cb909147152db427fe69dd7bcbf
Signed-off-by: Shawn O. Pearce <sop@google.com>
2010-07-15 16:52:42 -07:00
cc50bac8c7 Warn users before uploading if there are local changes
Change-Id: I231d7b6a3211e9f5ec71a542a0109b0c195d5e40
Signed-off-by: Shawn O. Pearce <sop@google.com>
2010-07-15 16:43:58 -07:00
0cb1b3f687 sync: Try fetching a tag as a last resort before giving up
If a tagged commit is not reachable by the fetch refspec configured
for the git (usually refs/heads/*) it will not be downloaded by
'git fetch'.  The tag can however be downloaded with 'git fetch
--tags' or 'git fetch tag <tag>'.

This patch fixes the situation when a tag is not found after a
'git fetch'. Repo will issue 'git fetch tag <tag>' before giving
up completely.

Change-Id: I87796a5e1d51fcf398f346a274b7a069df37599a
Signed-off-by: Shawn O. Pearce <sop@google.com>
2010-07-15 16:38:08 -07:00
9e426aa432 rebase: Automatically rebase branch on upstrea
Usage: repo rebase [[-i] <project>...]

Rebases the current topic branch of the specified (or all)
projects against the appropriate upstream.

Note: Interactive rebase is currently only supported when
exactly one project is specified on the command line.

Change-Id: I7376e35f27a6585149def82938c1ca99f36db2c4
Signed-off-by: Shawn O. Pearce <sop@google.com>
2010-07-15 16:35:31 -07:00
08a3f68d38 upload: Automatically --cc folks in review.URL.autocopy
The upload command will read review.URL.autocopy from the project's
configuration and append the list of e-mails specified to the
--cc argument of the upload command if a non-empty --re argument
was provided.

Change-Id: I2424517d17dd3444b20f0e6a003be6e70b8904f6
Signed-off-by: Shawn O. Pearce <sop@google.com>
2010-07-15 16:30:32 -07:00
feb39d61ef Fix format string bugs in grep
This fixes some format string bugs in grep which cause repo to with
"TypeError: not enough arguments for format string" when grepping and
the output contains a valid Python format string.

Change-Id: Ice8968ea106148d409490e4f71a2833b0cc80816
2010-06-17 19:09:37 -07:00
7198572dd7 Do not invoke ssh with -p argument when no port has been specified.
This change allows local SSH configuration to choose the port number
to use when not explicitly set in the manifest.

(cherry picked from commit 4c0f670465)

Change-Id: Ibea99cfe46b6a2cc27f754cc3944a2fe10f6fda4
2010-06-08 11:08:11 -07:00
2daf66740b Allow files to be copied into new folders
Change-Id: I7f169e32be5a4328bb87ce7c2ff4b6529e925126
2010-05-27 18:05:26 -07:00
f4f04d9fa8 Do not emit progress if stderr is not a tty
Avoids logging progress data into cron logs, etc.

Suggested-by: Michael Richardson <mcr@sandelman.ottawa.on.ca>
Change-Id: I4eefa2c282f0ca0a95a0185612b52e2146669e4c
Signed-off-by: Shawn O. Pearce <sop@google.com>
2010-05-27 16:48:36 -07:00
18afd7f679 sync: support --jobs to fetch projects simultaneously
This patch does two things for being compatibile with
those Python which are built without threading support:

1. As the Python document and Shawn suggested, import dummy_threading
   when the threading is not available.

2. Reserve the single threaded code and make it default.
   In cases the --jobs does not work properly with dummy_threading,
   we still have a safe fallback.

Change-Id: I40909ef8e9b5c22f315c0a1da9be38eed8b0a2dc
2010-05-27 14:54:20 -07:00
6623b21e10 Aliasing sync -s to 'smartsync'
This alias will let people use this command without having to
remember the option.

Change-Id: I3256d9e8e884c5be9e77f70e9cfb73e0f0c544c6
2010-05-17 09:58:55 -07:00
ca8c32cd7a sync: kill git fetch process before SSH control master process
If the SSH control master process is killed while an active git
fetch is using its network socket, the underlying SSH client may
not realize the connection was broken.  This can lead to both the
client and the server waiting indefinitely for network messages
which will never be sent.

Work around the problem by keeping track of any processes that use
the tunnels we establish.  If we are about to kill any of the SSH
control masters that we started, ensure the clients using them are
successfully killed first.

Change-Id: Ida6c124dcb0c6a26bf7dd69cba2fbdc2ecd5b2fc
Signed-off-by: Shawn O. Pearce <sop@google.com>
2010-05-11 18:31:47 -07:00
f0a9a1a30e upload: Move confirmation threshold from 3 to 5 commits
Change-Id: I7275d195cf04f02694206b9f838540b0228ff5e1
2010-05-05 09:20:51 -07:00
879a9a5cf0 upload: Confirm unusually large number of uploaded commit
Add a sentinel check to require a second explicit confirmation if the
user is attempting to upload (or upload --replace) an unusually large
number of commits.  This may help the user to catch an accidentally
incorrect rebase they had done previously.

Change-Id: I12c4d102f90a631d6ad193486a70ffd520ef6ae0
2010-05-04 17:15:37 -07:00
ff6929dde8 branches: Enable output of multiple projects
Fixes a bug introduced by 498a0e8a79
("Make 'repo branches -a' the default behavior").

Change-Id: Ib739f82f4647890c46d7c9fb2f2e63a16a0481de
2010-05-04 07:51:28 -07:00
1c85f4e43b Rename _ssh_sock() to fix code style issue.
Since _ssh_sock is imported out of the git_command module, the leading
underscore should be removed from the function name.
2010-04-27 14:35:27 -07:00
719965af35 Override manifest file only after it is fully written to disk.
We called "Override()" before closing the file passed in argument.

Change-Id: I15adb99deb14297ef72fcb1b0945eb246f172fb0
2010-04-26 11:20:22 -07:00
5732e47ebb Strip refs/heads in the branch sent to the manifest server.
The manifest server doesn't want to have refs/heads passed to it, so
we need to strip that when the branch contains it.

Change-Id: I044f8a9629220e886fd5e02e3c1ac4b4bb6020ba
2010-04-26 11:19:07 -07:00
f3fdf823cf sync: Safely skip already deleted projects
Do not error if a project is missing on the filesystem, is deleted
from manifest.xml, but still exists in project.list.

Change-Id: I1d13e435473c83091e27e4df571504ef493282dd
2010-04-14 14:21:50 -07:00
a1bfd2cd72 Add a 'smart sync' option to repo sync
This option allows the user to specify a manifest server to use when
syncing. This manifest server will provide a manifest pegging each
project to a known green build. This allows developers to work on a
known good tree that is known to build and pass tests, preventing
failed builds to hamper productivity.

The manifest used is not "sticky" so as to allow subsequent
'repo sync' calls to sync to the tip of the tree.

Change-Id: Id0a24ece20f5a88034ad364b416a1dd2e394226d
2010-04-13 10:20:37 -07:00
6d7508b3d5 Allow 'y' as a valid response when confirming identity
I prefer having to type only one character rather than all three,
and it seems like other confirmation prompts use the same style.
2010-04-01 11:30:56 -07:00
28 changed files with 1865 additions and 322 deletions

View File

@ -74,7 +74,7 @@ class Command(object):
project = all.get(arg)
if not project:
path = os.path.abspath(arg)
path = os.path.abspath(arg).replace('\\', '/')
if not by_path:
by_path = dict()
@ -82,13 +82,15 @@ class Command(object):
by_path[p.worktree] = p
if os.path.exists(path):
oldpath = None
while path \
and path != '/' \
and path != oldpath \
and path != self.manifest.topdir:
try:
project = by_path[path]
break
except KeyError:
oldpath = path
path = os.path.dirname(path)
else:
try:

View File

@ -20,10 +20,15 @@ A manifest XML file (e.g. 'default.xml') roughly conforms to the
following DTD:
<!DOCTYPE manifest [
<!ELEMENT manifest (remote*,
<!ELEMENT manifest (notice?,
remote*,
default?,
manifest-server?,
remove-project*,
project*)>
project*,
repo-hooks?)>
<!ELEMENT notice (#PCDATA)>
<!ELEMENT remote (EMPTY)>
<!ATTLIST remote name ID #REQUIRED>
@ -33,6 +38,10 @@ following DTD:
<!ELEMENT default (EMPTY)>
<!ATTLIST default remote IDREF #IMPLIED>
<!ATTLIST default revision CDATA #IMPLIED>
<!ATTLIST default sync-j CDATA #IMPLIED>
<!ELEMENT manifest-server (EMPTY)>
<!ATTLIST url CDATA #REQUIRED>
<!ELEMENT project (EMPTY)>
<!ATTLIST project name CDATA #REQUIRED>
@ -42,6 +51,10 @@ following DTD:
<!ELEMENT remove-project (EMPTY)>
<!ATTLIST remove-project name CDATA #REQUIRED>
<!ELEMENT repo-hooks (EMPTY)>
<!ATTLIST repo-hooks in-project CDATA #REQUIRED>
<!ATTLIST repo-hooks enabled-list CDATA #REQUIRED>
]>
A description of the elements and their attributes follows.
@ -89,6 +102,27 @@ Attribute `revision`: Name of a Git branch (e.g. `master` or
revision attribute will use this revision.
Element manifest-server
-----------------------
At most one manifest-server may be specified. The url attribute
is used to specify the URL of a manifest server, which is an
XML RPC service that will return a manifest in which each project
is pegged to a known good revision for the current branch and
target.
The manifest server should implement:
GetApprovedManifest(branch, target)
The target to use is defined by environment variables TARGET_PRODUCT
and TARGET_BUILD_VARIANT. These variables are used to create a string
of the form $TARGET_PRODUCT-$TARGET_BUILD_VARIANT, e.g. passion-userdebug.
If one of those variables or both are not present, the program will call
GetApprovedManifest without the target paramater and the manifest server
should choose a reasonable default target.
Element project
---------------

View File

@ -82,7 +82,7 @@ least one of these before using this command."""
fd = None
if re.compile("^.*[$ \t'].*$").match(editor):
args = [editor + ' "$@"']
args = [editor + ' "$@"', 'sh']
shell = True
else:
args = [editor]

View File

@ -75,3 +75,10 @@ class RepoChangedException(Exception):
"""
def __init__(self, extra_args=[]):
self.extra_args = extra_args
class HookError(Exception):
"""Thrown if a 'repo-hook' could not be run.
The common case is that the file wasn't present when we tried to run it.
"""
pass

View File

@ -17,6 +17,7 @@ import os
import sys
import subprocess
import tempfile
from signal import SIGTERM
from error import GitError
from trace import REPO_TRACE, IsTrace, Trace
@ -29,8 +30,9 @@ LAST_CWD = None
_ssh_proxy_path = None
_ssh_sock_path = None
_ssh_clients = []
def _ssh_sock(create=True):
def ssh_sock(create=True):
global _ssh_sock_path
if _ssh_sock_path is None:
if not create:
@ -51,6 +53,26 @@ def _ssh_proxy():
'git_ssh')
return _ssh_proxy_path
def _add_ssh_client(p):
_ssh_clients.append(p)
def _remove_ssh_client(p):
try:
_ssh_clients.remove(p)
except ValueError:
pass
def terminate_ssh_clients():
global _ssh_clients
for p in _ssh_clients:
try:
os.kill(p.pid, SIGTERM)
p.wait()
except OSError:
pass
_ssh_clients = []
_git_version = None
class _GitCall(object):
def version(self):
@ -59,6 +81,21 @@ class _GitCall(object):
return p.stdout
return None
def version_tuple(self):
global _git_version
if _git_version is None:
ver_str = git.version()
if ver_str.startswith('git version '):
_git_version = tuple(
map(lambda x: int(x),
ver_str[len('git version '):].strip().split('.')[0:3]
))
else:
print >>sys.stderr, 'fatal: "%s" unsupported' % ver_str
sys.exit(1)
return _git_version
def __getattr__(self, name):
name = name.replace('_','-')
def fun(*cmdv):
@ -68,23 +105,9 @@ class _GitCall(object):
return fun
git = _GitCall()
_git_version = None
def git_require(min_version, fail=False):
global _git_version
if _git_version is None:
ver_str = git.version()
if ver_str.startswith('git version '):
_git_version = tuple(
map(lambda x: int(x),
ver_str[len('git version '):].strip().split('.')[0:3]
))
else:
print >>sys.stderr, 'fatal: "%s" unsupported' % ver_str
sys.exit(1)
if min_version <= _git_version:
git_version = git.version_tuple()
if min_version <= git_version:
return True
if fail:
need = '.'.join(map(lambda x: str(x), min_version))
@ -92,6 +115,9 @@ def git_require(min_version, fail=False):
sys.exit(1)
return False
def _setenv(env, name, value):
env[name] = value.encode()
class GitCommand(object):
def __init__(self,
project,
@ -104,7 +130,7 @@ class GitCommand(object):
ssh_proxy = False,
cwd = None,
gitdir = None):
env = dict(os.environ)
env = os.environ.copy()
for e in [REPO_TRACE,
GIT_DIR,
@ -117,10 +143,10 @@ class GitCommand(object):
del env[e]
if disable_editor:
env['GIT_EDITOR'] = ':'
_setenv(env, 'GIT_EDITOR', ':')
if ssh_proxy:
env['REPO_SSH_SOCK'] = _ssh_sock()
env['GIT_SSH'] = _ssh_proxy()
_setenv(env, 'REPO_SSH_SOCK', ssh_sock())
_setenv(env, 'GIT_SSH', _ssh_proxy())
if project:
if not cwd:
@ -131,7 +157,7 @@ class GitCommand(object):
command = [GIT]
if bare:
if gitdir:
env[GIT_DIR] = gitdir
_setenv(env, GIT_DIR, gitdir)
cwd = None
command.extend(cmdv)
@ -188,6 +214,9 @@ class GitCommand(object):
except Exception, e:
raise GitError('%s: %s' % (command[1], e))
if ssh_proxy:
_add_ssh_client(p)
self.process = p
self.stdin = p.stdin
@ -210,4 +239,8 @@ class GitCommand(object):
else:
p.stderr = None
return self.process.wait()
try:
rc = p.wait()
finally:
_remove_ssh_client(p)
return rc

View File

@ -18,12 +18,21 @@ import os
import re
import subprocess
import sys
try:
import threading as _threading
except ImportError:
import dummy_threading as _threading
import time
import urllib2
from signal import SIGTERM
from urllib2 import urlopen, HTTPError
from error import GitError, UploadError
from trace import Trace
from git_command import GitCommand, _ssh_sock
from git_command import GitCommand
from git_command import ssh_sock
from git_command import terminate_ssh_clients
R_HEADS = 'refs/heads/'
R_TAGS = 'refs/tags/'
@ -189,6 +198,15 @@ class GitConfig(object):
except KeyError:
return False
def UrlInsteadOf(self, url):
"""Resolve any url.*.insteadof references.
"""
for new_url in self.GetSubSections('url'):
old_url = self.GetString('url.%s.insteadof' % new_url)
if old_url is not None and url.startswith(old_url):
return new_url + url[len(old_url):]
return url
@property
def _sections(self):
d = self._section_dict
@ -254,9 +272,11 @@ class GitConfig(object):
finally:
fd.close()
except IOError:
os.remove(self._pickle)
if os.path.exists(self._pickle):
os.remove(self._pickle)
except cPickle.PickleError:
os.remove(self._pickle)
if os.path.exists(self._pickle):
os.remove(self._pickle)
def _ReadGit(self):
"""
@ -353,59 +373,121 @@ class RefSpec(object):
return s
_ssh_cache = {}
_master_processes = []
_master_keys = set()
_ssh_master = True
_master_keys_lock = None
def _open_ssh(host, port):
def init_ssh():
"""Should be called once at the start of repo to init ssh master handling.
At the moment, all we do is to create our lock.
"""
global _master_keys_lock
assert _master_keys_lock is None, "Should only call init_ssh once"
_master_keys_lock = _threading.Lock()
def _open_ssh(host, port=None):
global _ssh_master
key = '%s:%s' % (host, port)
if key in _ssh_cache:
return True
if not _ssh_master \
or 'GIT_SSH' in os.environ \
or sys.platform in ('win32', 'cygwin'):
# failed earlier, or cygwin ssh can't do this
#
return False
command = ['ssh',
'-o','ControlPath %s' % _ssh_sock(),
'-p',str(port),
'-M',
'-N',
host]
# Acquire the lock. This is needed to prevent opening multiple masters for
# the same host when we're running "repo sync -jN" (for N > 1) _and_ the
# manifest <remote fetch="ssh://xyz"> specifies a different host from the
# one that was passed to repo init.
_master_keys_lock.acquire()
try:
Trace(': %s', ' '.join(command))
p = subprocess.Popen(command)
except Exception, e:
_ssh_master = False
print >>sys.stderr, \
'\nwarn: cannot enable ssh control master for %s:%s\n%s' \
% (host,port, str(e))
return False
_ssh_cache[key] = p
time.sleep(1)
return True
# Check to see whether we already think that the master is running; if we
# think it's already running, return right away.
if port is not None:
key = '%s:%s' % (host, port)
else:
key = host
if key in _master_keys:
return True
if not _ssh_master \
or 'GIT_SSH' in os.environ \
or sys.platform in ('win32', 'cygwin'):
# failed earlier, or cygwin ssh can't do this
#
return False
# We will make two calls to ssh; this is the common part of both calls.
command_base = ['ssh',
'-o','ControlPath %s' % ssh_sock(),
host]
if port is not None:
command_base[1:1] = ['-p',str(port)]
# Since the key wasn't in _master_keys, we think that master isn't running.
# ...but before actually starting a master, we'll double-check. This can
# be important because we can't tell that that 'git@myhost.com' is the same
# as 'myhost.com' where "User git" is setup in the user's ~/.ssh/config file.
check_command = command_base + ['-O','check']
try:
Trace(': %s', ' '.join(check_command))
check_process = subprocess.Popen(check_command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
check_process.communicate() # read output, but ignore it...
isnt_running = check_process.wait()
if not isnt_running:
# Our double-check found that the master _was_ infact running. Add to
# the list of keys.
_master_keys.add(key)
return True
except Exception:
# Ignore excpetions. We we will fall back to the normal command and print
# to the log there.
pass
command = command_base[:1] + \
['-M', '-N'] + \
command_base[1:]
try:
Trace(': %s', ' '.join(command))
p = subprocess.Popen(command)
except Exception, e:
_ssh_master = False
print >>sys.stderr, \
'\nwarn: cannot enable ssh control master for %s:%s\n%s' \
% (host,port, str(e))
return False
_master_processes.append(p)
_master_keys.add(key)
time.sleep(1)
return True
finally:
_master_keys_lock.release()
def close_ssh():
for key,p in _ssh_cache.iteritems():
global _master_keys_lock
terminate_ssh_clients()
for p in _master_processes:
try:
os.kill(p.pid, SIGTERM)
p.wait()
except OSError:
pass
_ssh_cache.clear()
del _master_processes[:]
_master_keys.clear()
d = _ssh_sock(create=False)
d = ssh_sock(create=False)
if d:
try:
os.rmdir(os.path.dirname(d))
except OSError:
pass
# We're done with the lock, so we can delete it.
_master_keys_lock = None
URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):')
URI_ALL = re.compile(r'^([a-z][a-z+]*)://([^@/]*@?[^/]*)/')
@ -417,7 +499,7 @@ def _preconnect(url):
if ':' in host:
host, port = host.split(':')
else:
port = 22
port = None
if scheme in ('ssh', 'git+ssh', 'ssh+git'):
return _open_ssh(host, port)
return False
@ -425,7 +507,7 @@ def _preconnect(url):
m = URI_SCP.match(url)
if m:
host = m.group(1)
return _open_ssh(host, 22)
return _open_ssh(host)
return False
@ -492,23 +574,25 @@ class Remote(object):
try:
info = urlopen(u).read()
if info == 'NOT_AVAILABLE':
raise UploadError('Upload over ssh unavailable')
raise UploadError('%s: SSH disabled' % self.review)
if '<' in info:
# Assume the server gave us some sort of HTML
# response back, like maybe a login page.
#
raise UploadError('Cannot read %s:\n%s' % (u, info))
raise UploadError('%s: Cannot parse response' % u)
self._review_protocol = 'ssh'
self._review_host = info.split(" ")[0]
self._review_port = info.split(" ")[1]
except urllib2.URLError, e:
raise UploadError('%s: %s' % (self.review, e.reason[1]))
except HTTPError, e:
if e.code == 404:
self._review_protocol = 'http-post'
self._review_host = None
self._review_port = None
else:
raise UploadError('Cannot guess Gerrit version')
raise UploadError('Upload over ssh unavailable')
REVIEW_CACHE[u] = (
self._review_protocol,
@ -519,8 +603,11 @@ class Remote(object):
def SshReviewUrl(self, userEmail):
if self.ReviewProtocol != 'ssh':
return None
username = self._config.GetString('review.%s.username' % self.review)
if username is None:
username = userEmail.split("@")[0]
return 'ssh://%s@%s:%s/%s' % (
userEmail.split("@")[0],
username,
self._review_host,
self._review_port,
self.projectname)

View File

@ -1,2 +1,2 @@
#!/bin/sh
exec ssh -o "ControlPath $REPO_SSH_SOCK" "$@"
exec ssh -o "ControlMaster no" -o "ControlPath $REPO_SSH_SOCK" "$@"

100
main.py
View File

@ -22,13 +22,17 @@ if __name__ == '__main__':
del sys.argv[-1]
del magic
import netrc
import optparse
import os
import re
import sys
import time
import urllib2
from trace import SetTrace
from git_config import close_ssh
from git_command import git, GitCommand
from git_config import init_ssh, close_ssh
from command import InteractiveCommand
from command import MirrorSafeCommand
from command import PagedCommand
@ -53,6 +57,9 @@ global_options.add_option('--no-pager',
global_options.add_option('--trace',
dest='trace', action='store_true',
help='trace git command execution')
global_options.add_option('--time',
dest='time', action='store_true',
help='time repo command execution')
global_options.add_option('--version',
dest='show_version', action='store_true',
help='display this version of repo')
@ -61,6 +68,8 @@ class _Repo(object):
def __init__(self, repodir):
self.repodir = repodir
self.commands = all_commands
# add 'branch' as an alias for 'branches'
all_commands['branch'] = all_commands['branches']
def _Run(self, argv):
name = None
@ -120,7 +129,20 @@ class _Repo(object):
RunPager(config)
try:
cmd.Execute(copts, cargs)
start = time.time()
try:
cmd.Execute(copts, cargs)
finally:
elapsed = time.time() - start
hours, remainder = divmod(elapsed, 3600)
minutes, seconds = divmod(remainder, 60)
if gopts.time:
if hours == 0:
print >>sys.stderr, 'real\t%dm%.3fs' \
% (minutes, seconds)
else:
print >>sys.stderr, 'real\t%dh%dm%.3fs' \
% (hours, minutes, seconds)
except ManifestInvalidRevisionError, e:
print >>sys.stderr, 'error: %s' % str(e)
sys.exit(1)
@ -131,6 +153,9 @@ class _Repo(object):
print >>sys.stderr, 'error: no project in current directory'
sys.exit(1)
def _MyRepoPath():
return os.path.dirname(__file__)
def _MyWrapperPath():
return os.path.join(os.path.dirname(__file__), 'repo')
@ -197,6 +222,75 @@ def _PruneOptions(argv, opt):
continue
i += 1
_user_agent = None
def _UserAgent():
global _user_agent
if _user_agent is None:
py_version = sys.version_info
os_name = sys.platform
if os_name == 'linux2':
os_name = 'Linux'
elif os_name == 'win32':
os_name = 'Win32'
elif os_name == 'cygwin':
os_name = 'Cygwin'
elif os_name == 'darwin':
os_name = 'Darwin'
p = GitCommand(
None, ['describe', 'HEAD'],
cwd = _MyRepoPath(),
capture_stdout = True)
if p.Wait() == 0:
repo_version = p.stdout
if len(repo_version) > 0 and repo_version[-1] == '\n':
repo_version = repo_version[0:-1]
if len(repo_version) > 0 and repo_version[0] == 'v':
repo_version = repo_version[1:]
else:
repo_version = 'unknown'
_user_agent = 'git-repo/%s (%s) git/%s Python/%d.%d.%d' % (
repo_version,
os_name,
'.'.join(map(lambda d: str(d), git.version_tuple())),
py_version[0], py_version[1], py_version[2])
return _user_agent
class _UserAgentHandler(urllib2.BaseHandler):
def http_request(self, req):
req.add_header('User-Agent', _UserAgent())
return req
def https_request(self, req):
req.add_header('User-Agent', _UserAgent())
return req
def init_http():
handlers = [_UserAgentHandler()]
mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
try:
n = netrc.netrc()
for host in n.hosts:
p = n.hosts[host]
mgr.add_password(None, 'http://%s/' % host, p[0], p[2])
mgr.add_password(None, 'https://%s/' % host, p[0], p[2])
except netrc.NetrcParseError:
pass
handlers.append(urllib2.HTTPBasicAuthHandler(mgr))
if 'http_proxy' in os.environ:
url = os.environ['http_proxy']
handlers.append(urllib2.ProxyHandler({'http': url, 'https': url}))
if 'REPO_CURL_VERBOSE' in os.environ:
handlers.append(urllib2.HTTPHandler(debuglevel=1))
handlers.append(urllib2.HTTPSHandler(debuglevel=1))
urllib2.install_opener(urllib2.build_opener(*handlers))
def _Main(argv):
opt = optparse.OptionParser(usage="repo wrapperinfo -- ...")
opt.add_option("--repo-dir", dest="repodir",
@ -214,6 +308,8 @@ def _Main(argv):
repo = _Repo(opt.repodir)
try:
try:
init_ssh()
init_http()
repo._Run(argv)
finally:
close_ssh()

View File

@ -29,6 +29,7 @@ class _Default(object):
revisionExpr = None
remote = None
sync_j = 1
class _XmlRemote(object):
def __init__(self,
@ -65,8 +66,8 @@ class XmlManifest(object):
self._Unload()
def Link(self, name):
"""Update the repo metadata to use a different manifest.
def Override(self, name):
"""Use a different manifest, just for the current instantiation.
"""
path = os.path.join(self.manifestProject.worktree, name)
if not os.path.isfile(path):
@ -80,6 +81,11 @@ class XmlManifest(object):
finally:
self.manifestFile = old
def Link(self, name):
"""Update the repo metadata to use a different manifest.
"""
self.Override(name)
try:
if os.path.exists(self.manifestFile):
os.remove(self.manifestFile)
@ -102,6 +108,15 @@ class XmlManifest(object):
root = doc.createElement('manifest')
doc.appendChild(root)
# Save out the notice. There's a little bit of work here to give it the
# right whitespace, which assumes that the notice is automatically indented
# by 4 by minidom.
if self.notice:
notice_element = root.appendChild(doc.createElement('notice'))
notice_lines = self.notice.splitlines()
indented_notice = ('\n'.join(" "*4 + line for line in notice_lines))[4:]
notice_element.appendChild(doc.createTextNode(indented_notice))
d = self.default
sort_remotes = list(self.remotes.keys())
sort_remotes.sort()
@ -119,10 +134,19 @@ class XmlManifest(object):
if d.revisionExpr:
have_default = True
e.setAttribute('revision', d.revisionExpr)
if d.sync_j > 1:
have_default = True
e.setAttribute('sync-j', '%d' % d.sync_j)
if have_default:
root.appendChild(e)
root.appendChild(doc.createTextNode(''))
if self._manifest_server:
e = doc.createElement('manifest-server')
e.setAttribute('url', self._manifest_server)
root.appendChild(e)
root.appendChild(doc.createTextNode(''))
sort_projects = list(self.projects.keys())
sort_projects.sort()
@ -151,6 +175,14 @@ class XmlManifest(object):
ce.setAttribute('dest', c.dest)
e.appendChild(ce)
if self._repo_hooks_project:
root.appendChild(doc.createTextNode(''))
e = doc.createElement('repo-hooks')
e.setAttribute('in-project', self._repo_hooks_project.name)
e.setAttribute('enabled-list',
' '.join(self._repo_hooks_project.enabled_repo_hooks))
root.appendChild(e)
doc.writexml(fd, '', ' ', '\n', 'UTF-8')
@property
@ -168,6 +200,21 @@ class XmlManifest(object):
self._Load()
return self._default
@property
def repo_hooks_project(self):
self._Load()
return self._repo_hooks_project
@property
def notice(self):
self._Load()
return self._notice
@property
def manifest_server(self):
self._Load()
return self._manifest_server
@property
def IsMirror(self):
return self.manifestProject.config.GetBoolean('repo.mirror')
@ -177,7 +224,10 @@ class XmlManifest(object):
self._projects = {}
self._remotes = {}
self._default = None
self._repo_hooks_project = None
self._notice = None
self.branch = None
self._manifest_server = None
def _Load(self):
if not self._loaded:
@ -207,15 +257,15 @@ class XmlManifest(object):
def _ParseManifest(self, is_root_file):
root = xml.dom.minidom.parse(self.manifestFile)
if not root or not root.childNodes:
raise ManifestParseError, \
"no root node in %s" % \
self.manifestFile
raise ManifestParseError(
"no root node in %s" %
self.manifestFile)
config = root.childNodes[0]
if config.nodeName != 'manifest':
raise ManifestParseError, \
"no <manifest> in %s" % \
self.manifestFile
raise ManifestParseError(
"no <manifest> in %s" %
self.manifestFile)
for node in config.childNodes:
if node.nodeName == 'remove-project':
@ -223,38 +273,83 @@ class XmlManifest(object):
try:
del self._projects[name]
except KeyError:
raise ManifestParseError, \
'project %s not found' % \
(name)
raise ManifestParseError(
'project %s not found' %
(name))
# If the manifest removes the hooks project, treat it as if it deleted
# the repo-hooks element too.
if self._repo_hooks_project and (self._repo_hooks_project.name == name):
self._repo_hooks_project = None
for node in config.childNodes:
if node.nodeName == 'remote':
remote = self._ParseRemote(node)
if self._remotes.get(remote.name):
raise ManifestParseError, \
'duplicate remote %s in %s' % \
(remote.name, self.manifestFile)
raise ManifestParseError(
'duplicate remote %s in %s' %
(remote.name, self.manifestFile))
self._remotes[remote.name] = remote
for node in config.childNodes:
if node.nodeName == 'default':
if self._default is not None:
raise ManifestParseError, \
'duplicate default in %s' % \
(self.manifestFile)
raise ManifestParseError(
'duplicate default in %s' %
(self.manifestFile))
self._default = self._ParseDefault(node)
if self._default is None:
self._default = _Default()
for node in config.childNodes:
if node.nodeName == 'notice':
if self._notice is not None:
raise ManifestParseError(
'duplicate notice in %s' %
(self.manifestFile))
self._notice = self._ParseNotice(node)
for node in config.childNodes:
if node.nodeName == 'manifest-server':
url = self._reqatt(node, 'url')
if self._manifest_server is not None:
raise ManifestParseError(
'duplicate manifest-server in %s' %
(self.manifestFile))
self._manifest_server = url
for node in config.childNodes:
if node.nodeName == 'project':
project = self._ParseProject(node)
if self._projects.get(project.name):
raise ManifestParseError, \
'duplicate project %s in %s' % \
(project.name, self.manifestFile)
raise ManifestParseError(
'duplicate project %s in %s' %
(project.name, self.manifestFile))
self._projects[project.name] = project
for node in config.childNodes:
if node.nodeName == 'repo-hooks':
# Get the name of the project and the (space-separated) list of enabled.
repo_hooks_project = self._reqatt(node, 'in-project')
enabled_repo_hooks = self._reqatt(node, 'enabled-list').split()
# Only one project can be the hooks project
if self._repo_hooks_project is not None:
raise ManifestParseError(
'duplicate repo-hooks in %s' %
(self.manifestFile))
# Store a reference to the Project.
try:
self._repo_hooks_project = self._projects[repo_hooks_project]
except KeyError:
raise ManifestParseError(
'project %s not found for repo-hooks' %
(repo_hooks_project))
# Store the enabled hooks in the Project object.
self._repo_hooks_project.enabled_repo_hooks = enabled_repo_hooks
def _AddMetaProjectMirror(self, m):
name = None
m_url = m.GetRemote(m.remote.name).url
@ -310,12 +405,56 @@ class XmlManifest(object):
d.revisionExpr = node.getAttribute('revision')
if d.revisionExpr == '':
d.revisionExpr = None
sync_j = node.getAttribute('sync-j')
if sync_j == '' or sync_j is None:
d.sync_j = 1
else:
d.sync_j = int(sync_j)
return d
def _ParseNotice(self, node):
"""
reads a <notice> element from the manifest file
The <notice> element is distinct from other tags in the XML in that the
data is conveyed between the start and end tag (it's not an empty-element
tag).
The white space (carriage returns, indentation) for the notice element is
relevant and is parsed in a way that is based on how python docstrings work.
In fact, the code is remarkably similar to here:
http://www.python.org/dev/peps/pep-0257/
"""
# Get the data out of the node...
notice = node.childNodes[0].data
# Figure out minimum indentation, skipping the first line (the same line
# as the <notice> tag)...
minIndent = sys.maxint
lines = notice.splitlines()
for line in lines[1:]:
lstrippedLine = line.lstrip()
if lstrippedLine:
indent = len(line) - len(lstrippedLine)
minIndent = min(indent, minIndent)
# Strip leading / trailing blank lines and also indentation.
cleanLines = [lines[0].strip()]
for line in lines[1:]:
cleanLines.append(line[minIndent:].rstrip())
# Clear completely blank lines from front and back...
while cleanLines and not cleanLines[0]:
del cleanLines[0]
while cleanLines and not cleanLines[-1]:
del cleanLines[-1]
return '\n'.join(cleanLines)
def _ParseProject(self, node):
"""
reads a <project> element from the manifest file
"""
"""
name = self._reqatt(node, 'name')
remote = self._get_remote(node)
@ -347,7 +486,7 @@ class XmlManifest(object):
worktree = None
gitdir = os.path.join(self.topdir, '%s.git' % name)
else:
worktree = os.path.join(self.topdir, path)
worktree = os.path.join(self.topdir, path).replace('\\', '/')
gitdir = os.path.join(self.repodir, 'projects/%s.git' % path)
project = Project(manifest = self,

View File

@ -13,23 +13,27 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import sys
from time import time
from trace import IsTrace
_NOT_TTY = not os.isatty(2)
class Progress(object):
def __init__(self, title, total=0):
def __init__(self, title, total=0, units=''):
self._title = title
self._total = total
self._done = 0
self._lastp = -1
self._start = time()
self._show = False
self._units = units
def update(self, inc=1):
self._done += inc
if IsTrace():
if _NOT_TTY or IsTrace():
return
if not self._show:
@ -48,15 +52,15 @@ class Progress(object):
if self._lastp != p:
self._lastp = p
sys.stderr.write('\r%s: %3d%% (%d/%d) ' % (
sys.stderr.write('\r%s: %3d%% (%d%s/%d%s) ' % (
self._title,
p,
self._done,
self._total))
self._done, self._units,
self._total, self._units))
sys.stderr.flush()
def end(self):
if IsTrace() or not self._show:
if _NOT_TTY or IsTrace() or not self._show:
return
if self._total <= 0:
@ -66,9 +70,9 @@ class Progress(object):
sys.stderr.flush()
else:
p = (100 * self._done) / self._total
sys.stderr.write('\r%s: %3d%% (%d/%d), done. \n' % (
sys.stderr.write('\r%s: %3d%% (%d%s/%d%s), done. \n' % (
self._title,
p,
self._done,
self._total))
self._done, self._units,
self._total, self._units))
sys.stderr.flush()

View File

@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import traceback
import errno
import filecmp
import os
@ -24,7 +25,7 @@ import urllib2
from color import Coloring
from git_command import GitCommand
from git_config import GitConfig, IsId
from error import GitError, ImportError, UploadError
from error import GitError, HookError, ImportError, UploadError
from error import ManifestInvalidRevisionError
from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M
@ -54,14 +55,25 @@ def not_rev(r):
def sq(r):
return "'" + r.replace("'", "'\''") + "'"
hook_list = None
def repo_hooks():
global hook_list
if hook_list is None:
_project_hook_list = None
def _ProjectHooks():
"""List the hooks present in the 'hooks' directory.
These hooks are project hooks and are copied to the '.git/hooks' directory
of all subprojects.
This function caches the list of hooks (based on the contents of the
'repo/hooks' directory) on the first call.
Returns:
A list of absolute paths to all of the files in the hooks directory.
"""
global _project_hook_list
if _project_hook_list is None:
d = os.path.abspath(os.path.dirname(__file__))
d = os.path.join(d , 'hooks')
hook_list = map(lambda x: os.path.join(d, x), os.listdir(d))
return hook_list
_project_hook_list = map(lambda x: os.path.join(d, x), os.listdir(d))
return _project_hook_list
def relpath(dst, src):
src = os.path.dirname(src)
@ -111,7 +123,6 @@ class ReviewableBranch(object):
self.project = project
self.branch = branch
self.base = base
self.replace_changes = None
@property
def name(self):
@ -149,10 +160,10 @@ class ReviewableBranch(object):
R_HEADS + self.name,
'--')
def UploadForReview(self, people):
def UploadForReview(self, people, auto_topic=False):
self.project.UploadForReview(self.name,
self.replace_changes,
people)
people,
auto_topic=auto_topic)
def GetPublishedRefs(self):
refs = {}
@ -203,6 +214,10 @@ class _CopyFile:
# remove existing file first, since it might be read-only
if os.path.exists(dest):
os.remove(dest)
else:
dir = os.path.dirname(dest)
if not os.path.isdir(dir):
os.makedirs(dir)
shutil.copy(src, dest)
# make the file read-only
mode = os.stat(dest)[stat.ST_MODE]
@ -220,6 +235,249 @@ class RemoteSpec(object):
self.url = url
self.review = review
class RepoHook(object):
"""A RepoHook contains information about a script to run as a hook.
Hooks are used to run a python script before running an upload (for instance,
to run presubmit checks). Eventually, we may have hooks for other actions.
This shouldn't be confused with files in the 'repo/hooks' directory. Those
files are copied into each '.git/hooks' folder for each project. Repo-level
hooks are associated instead with repo actions.
Hooks are always python. When a hook is run, we will load the hook into the
interpreter and execute its main() function.
"""
def __init__(self,
hook_type,
hooks_project,
topdir,
abort_if_user_denies=False):
"""RepoHook constructor.
Params:
hook_type: A string representing the type of hook. This is also used
to figure out the name of the file containing the hook. For
example: 'pre-upload'.
hooks_project: The project containing the repo hooks. If you have a
manifest, this is manifest.repo_hooks_project. OK if this is None,
which will make the hook a no-op.
topdir: Repo's top directory (the one containing the .repo directory).
Scripts will run with CWD as this directory. If you have a manifest,
this is manifest.topdir
abort_if_user_denies: If True, we'll throw a HookError() if the user
doesn't allow us to run the hook.
"""
self._hook_type = hook_type
self._hooks_project = hooks_project
self._topdir = topdir
self._abort_if_user_denies = abort_if_user_denies
# Store the full path to the script for convenience.
if self._hooks_project:
self._script_fullpath = os.path.join(self._hooks_project.worktree,
self._hook_type + '.py')
else:
self._script_fullpath = None
def _GetHash(self):
"""Return a hash of the contents of the hooks directory.
We'll just use git to do this. This hash has the property that if anything
changes in the directory we will return a different has.
SECURITY CONSIDERATION:
This hash only represents the contents of files in the hook directory, not
any other files imported or called by hooks. Changes to imported files
can change the script behavior without affecting the hash.
Returns:
A string representing the hash. This will always be ASCII so that it can
be printed to the user easily.
"""
assert self._hooks_project, "Must have hooks to calculate their hash."
# We will use the work_git object rather than just calling GetRevisionId().
# That gives us a hash of the latest checked in version of the files that
# the user will actually be executing. Specifically, GetRevisionId()
# doesn't appear to change even if a user checks out a different version
# of the hooks repo (via git checkout) nor if a user commits their own revs.
#
# NOTE: Local (non-committed) changes will not be factored into this hash.
# I think this is OK, since we're really only worried about warning the user
# about upstream changes.
return self._hooks_project.work_git.rev_parse('HEAD')
def _GetMustVerb(self):
"""Return 'must' if the hook is required; 'should' if not."""
if self._abort_if_user_denies:
return 'must'
else:
return 'should'
def _CheckForHookApproval(self):
"""Check to see whether this hook has been approved.
We'll look at the hash of all of the hooks. If this matches the hash that
the user last approved, we're done. If it doesn't, we'll ask the user
about approval.
Note that we ask permission for each individual hook even though we use
the hash of all hooks when detecting changes. We'd like the user to be
able to approve / deny each hook individually. We only use the hash of all
hooks because there is no other easy way to detect changes to local imports.
Returns:
True if this hook is approved to run; False otherwise.
Raises:
HookError: Raised if the user doesn't approve and abort_if_user_denies
was passed to the consturctor.
"""
hooks_dir = self._hooks_project.worktree
hooks_config = self._hooks_project.config
git_approval_key = 'repo.hooks.%s.approvedhash' % self._hook_type
# Get the last hash that the user approved for this hook; may be None.
old_hash = hooks_config.GetString(git_approval_key)
# Get the current hash so we can tell if scripts changed since approval.
new_hash = self._GetHash()
if old_hash is not None:
# User previously approved hook and asked not to be prompted again.
if new_hash == old_hash:
# Approval matched. We're done.
return True
else:
# Give the user a reason why we're prompting, since they last told
# us to "never ask again".
prompt = 'WARNING: Scripts have changed since %s was allowed.\n\n' % (
self._hook_type)
else:
prompt = ''
# Prompt the user if we're not on a tty; on a tty we'll assume "no".
if sys.stdout.isatty():
prompt += ('Repo %s run the script:\n'
' %s\n'
'\n'
'Do you want to allow this script to run '
'(yes/yes-never-ask-again/NO)? ') % (
self._GetMustVerb(), self._script_fullpath)
response = raw_input(prompt).lower()
print
# User is doing a one-time approval.
if response in ('y', 'yes'):
return True
elif response == 'yes-never-ask-again':
hooks_config.SetString(git_approval_key, new_hash)
return True
# For anything else, we'll assume no approval.
if self._abort_if_user_denies:
raise HookError('You must allow the %s hook or use --no-verify.' %
self._hook_type)
return False
def _ExecuteHook(self, **kwargs):
"""Actually execute the given hook.
This will run the hook's 'main' function in our python interpreter.
Args:
kwargs: Keyword arguments to pass to the hook. These are often specific
to the hook type. For instance, pre-upload hooks will contain
a project_list.
"""
# Keep sys.path and CWD stashed away so that we can always restore them
# upon function exit.
orig_path = os.getcwd()
orig_syspath = sys.path
try:
# Always run hooks with CWD as topdir.
os.chdir(self._topdir)
# Put the hook dir as the first item of sys.path so hooks can do
# relative imports. We want to replace the repo dir as [0] so
# hooks can't import repo files.
sys.path = [os.path.dirname(self._script_fullpath)] + sys.path[1:]
# Exec, storing global context in the context dict. We catch exceptions
# and convert to a HookError w/ just the failing traceback.
context = {}
try:
execfile(self._script_fullpath, context)
except Exception:
raise HookError('%s\nFailed to import %s hook; see traceback above.' % (
traceback.format_exc(), self._hook_type))
# Running the script should have defined a main() function.
if 'main' not in context:
raise HookError('Missing main() in: "%s"' % self._script_fullpath)
# Add 'hook_should_take_kwargs' to the arguments to be passed to main.
# We don't actually want hooks to define their main with this argument--
# it's there to remind them that their hook should always take **kwargs.
# For instance, a pre-upload hook should be defined like:
# def main(project_list, **kwargs):
#
# This allows us to later expand the API without breaking old hooks.
kwargs = kwargs.copy()
kwargs['hook_should_take_kwargs'] = True
# Call the main function in the hook. If the hook should cause the
# build to fail, it will raise an Exception. We'll catch that convert
# to a HookError w/ just the failing traceback.
try:
context['main'](**kwargs)
except Exception:
raise HookError('%s\nFailed to run main() for %s hook; see traceback '
'above.' % (
traceback.format_exc(), self._hook_type))
finally:
# Restore sys.path and CWD.
sys.path = orig_syspath
os.chdir(orig_path)
def Run(self, user_allows_all_hooks, **kwargs):
"""Run the hook.
If the hook doesn't exist (because there is no hooks project or because
this particular hook is not enabled), this is a no-op.
Args:
user_allows_all_hooks: If True, we will never prompt about running the
hook--we'll just assume it's OK to run it.
kwargs: Keyword arguments to pass to the hook. These are often specific
to the hook type. For instance, pre-upload hooks will contain
a project_list.
Raises:
HookError: If there was a problem finding the hook or the user declined
to run a required hook (from _CheckForHookApproval).
"""
# No-op if there is no hooks project or if hook is disabled.
if ((not self._hooks_project) or
(self._hook_type not in self._hooks_project.enabled_repo_hooks)):
return
# Bail with a nice error if we can't find the hook.
if not os.path.isfile(self._script_fullpath):
raise HookError('Couldn\'t find repo hook: "%s"' % self._script_fullpath)
# Make sure the user is OK with running the hook.
if (not user_allows_all_hooks) and (not self._CheckForHookApproval()):
return
# Run the hook with the same version of python we're using.
self._ExecuteHook(**kwargs)
class Project(object):
def __init__(self,
manifest,
@ -233,8 +491,11 @@ class Project(object):
self.manifest = manifest
self.name = name
self.remote = remote
self.gitdir = gitdir
self.worktree = worktree
self.gitdir = gitdir.replace('\\', '/')
if worktree:
self.worktree = worktree.replace('\\', '/')
else:
self.worktree = None
self.relpath = relpath
self.revisionExpr = revisionExpr
@ -258,6 +519,10 @@ class Project(object):
self.bare_git = self._GitGetByExec(self, bare=True)
self.bare_ref = GitRefs(gitdir)
# This will be filled in if a project is later identified to be the
# project containing repo hooks.
self.enabled_repo_hooks = []
@property
def Exists(self):
return os.path.isdir(self.gitdir)
@ -279,7 +544,7 @@ class Project(object):
return os.path.exists(os.path.join(g, 'rebase-apply')) \
or os.path.exists(os.path.join(g, 'rebase-merge')) \
or os.path.exists(os.path.join(w, '.dotest'))
def IsDirty(self, consider_untracked=True):
"""Is the working directory modified in some way?
"""
@ -364,13 +629,39 @@ class Project(object):
## Status Display ##
def PrintWorkTreeStatus(self):
def HasChanges(self):
"""Returns true if there are uncommitted changes.
"""
self.work_git.update_index('-q',
'--unmerged',
'--ignore-missing',
'--refresh')
if self.IsRebaseInProgress():
return True
if self.work_git.DiffZ('diff-index', '--cached', HEAD):
return True
if self.work_git.DiffZ('diff-files'):
return True
if self.work_git.LsOthers():
return True
return False
def PrintWorkTreeStatus(self, output_redir=None):
"""Prints the status of the repository to stdout.
Args:
output: If specified, redirect the output to this object.
"""
if not os.path.isdir(self.worktree):
print ''
print 'project %s/' % self.relpath
print ' missing (run "repo sync")'
if output_redir == None:
output_redir = sys.stdout
print >>output_redir, ''
print >>output_redir, 'project %s/' % self.relpath
print >>output_redir, ' missing (run "repo sync")'
return
self.work_git.update_index('-q',
@ -385,6 +676,8 @@ class Project(object):
return 'CLEAN'
out = StatusColoring(self.config)
if not output_redir == None:
out.redirect(output_redir)
out.project('project %-40s', self.relpath + '/')
branch = self.CurrentBranch
@ -412,7 +705,7 @@ class Project(object):
try: f = df[p]
except KeyError: f = None
if i: i_status = i.status.upper()
else: i_status = '-'
@ -434,6 +727,7 @@ class Project(object):
else:
out.write('%s', line)
out.nl()
return 'DIRTY'
def PrintWorkTreeDiff(self):
@ -497,7 +791,7 @@ class Project(object):
if R_HEADS + n not in heads:
self.bare_git.DeleteRef(name, id)
def GetUploadableBranches(self):
def GetUploadableBranches(self, selected_branch=None):
"""List any branches which can be uploaded for review.
"""
heads = {}
@ -513,6 +807,8 @@ class Project(object):
for branch, id in heads.iteritems():
if branch in pubed and pubed[branch] == id:
continue
if selected_branch and branch != selected_branch:
continue
rb = self.GetUploadableBranch(branch)
if rb:
@ -530,7 +826,9 @@ class Project(object):
return rb
return None
def UploadForReview(self, branch=None, replace_changes=None, people=([],[])):
def UploadForReview(self, branch=None,
people=([],[]),
auto_topic=False):
"""Uploads the named branch for code review.
"""
if branch is None:
@ -562,13 +860,15 @@ class Project(object):
for e in people[1]:
rp.append('--cc=%s' % sq(e))
ref_spec = '%s:refs/for/%s' % (R_HEADS + branch.name, dest_branch)
if auto_topic:
ref_spec = ref_spec + '/' + branch.name
cmd = ['push']
cmd.append('--receive-pack=%s' % " ".join(rp))
cmd.append(branch.remote.SshReviewUrl(self.UserEmail))
cmd.append('%s:refs/for/%s' % (R_HEADS + branch.name, dest_branch))
if replace_changes:
for change_id,commit_id in replace_changes.iteritems():
cmd.append('%s:refs/changes/%s/new' % (commit_id, change_id))
cmd.append(ref_spec)
if GitCommand(self, cmd, bare = True).Wait() != 0:
raise UploadError('Upload failed')
@ -584,19 +884,33 @@ class Project(object):
## Sync ##
def Sync_NetworkHalf(self):
def Sync_NetworkHalf(self, quiet=False):
"""Perform only the network IO portion of the sync process.
Local working directory/branch state is not affected.
"""
if not self.Exists:
print >>sys.stderr
print >>sys.stderr, 'Initializing project %s ...' % self.name
is_new = not self.Exists
if is_new:
if not quiet:
print >>sys.stderr
print >>sys.stderr, 'Initializing project %s ...' % self.name
self._InitGitDir()
self._InitRemote()
if not self._RemoteFetch():
if not self._RemoteFetch(initial=is_new, quiet=quiet):
return False
#Check that the requested ref was found after fetch
#
try:
self.GetRevisionId()
except ManifestInvalidRevisionError:
# if the ref is a tag. We can try fetching
# the tag manually as a last resort
#
rev = self.revisionExpr
if rev.startswith(R_TAGS):
self._RemoteFetch(None, rev[len(R_TAGS):], quiet=quiet)
if self.worktree:
self._InitMRef()
else:
@ -635,11 +949,11 @@ class Project(object):
"""Perform only the local IO portion of the sync process.
Network access is not required.
"""
self._InitWorkTree()
all = self.bare_ref.all
self.CleanPublishedCache(all)
revid = self.GetRevisionId(all)
self._InitWorkTree()
head = self.work_git.GetHead()
if head.startswith(R_HEADS):
branch = head[len(R_HEADS):]
@ -855,6 +1169,13 @@ class Project(object):
def CheckoutBranch(self, name):
"""Checkout a local topic branch.
Args:
name: The name of the branch to checkout.
Returns:
True if the checkout succeeded; False if it didn't; None if the branch
didn't exist.
"""
rev = R_HEADS + name
head = self.work_git.GetHead()
@ -869,7 +1190,7 @@ class Project(object):
except KeyError:
# Branch does not exist in this project
#
return False
return None
if head.startswith(R_HEADS):
try:
@ -892,13 +1213,19 @@ class Project(object):
def AbandonBranch(self, name):
"""Destroy a local topic branch.
Args:
name: The name of the branch to abandon.
Returns:
True if the abandon succeeded; False if it didn't; None if the branch
didn't exist.
"""
rev = R_HEADS + name
all = self.bare_ref.all
if rev not in all:
# Doesn't exist; assume already abandoned.
#
return True
# Doesn't exist
return None
head = self.work_git.GetHead()
if head == rev:
@ -978,7 +1305,9 @@ class Project(object):
## Direct Git Commands ##
def _RemoteFetch(self, name=None):
def _RemoteFetch(self, name=None, tag=None,
initial=False,
quiet=False):
if not name:
name = self.remote.name
@ -986,14 +1315,91 @@ class Project(object):
if self.GetRemote(name).PreConnectFetch():
ssh_proxy = True
if initial:
alt = os.path.join(self.gitdir, 'objects/info/alternates')
try:
fd = open(alt, 'rb')
try:
ref_dir = fd.readline()
if ref_dir and ref_dir.endswith('\n'):
ref_dir = ref_dir[:-1]
finally:
fd.close()
except IOError, e:
ref_dir = None
if ref_dir and 'objects' == os.path.basename(ref_dir):
ref_dir = os.path.dirname(ref_dir)
packed_refs = os.path.join(self.gitdir, 'packed-refs')
remote = self.GetRemote(name)
all = self.bare_ref.all
ids = set(all.values())
tmp = set()
for r, id in GitRefs(ref_dir).all.iteritems():
if r not in all:
if r.startswith(R_TAGS) or remote.WritesTo(r):
all[r] = id
ids.add(id)
continue
if id in ids:
continue
r = 'refs/_alt/%s' % id
all[r] = id
ids.add(id)
tmp.add(r)
ref_names = list(all.keys())
ref_names.sort()
tmp_packed = ''
old_packed = ''
for r in ref_names:
line = '%s %s\n' % (all[r], r)
tmp_packed += line
if r not in tmp:
old_packed += line
_lwrite(packed_refs, tmp_packed)
else:
ref_dir = None
cmd = ['fetch']
# The --depth option only affects the initial fetch; after that we'll do
# full fetches of changes.
depth = self.manifest.manifestProject.config.GetString('repo.depth')
if depth and initial:
cmd.append('--depth=%s' % depth)
if quiet:
cmd.append('--quiet')
if not self.worktree:
cmd.append('--update-head-ok')
cmd.append(name)
return GitCommand(self,
cmd,
bare = True,
ssh_proxy = ssh_proxy).Wait() == 0
if tag is not None:
cmd.append('tag')
cmd.append(tag)
ok = GitCommand(self,
cmd,
bare = True,
ssh_proxy = ssh_proxy).Wait() == 0
if initial:
if ref_dir:
if old_packed != '':
_lwrite(packed_refs, old_packed)
else:
os.remove(packed_refs)
self.bare_git.pack_refs('--all', '--prune')
return ok
def _Checkout(self, rev, quiet=False):
cmd = ['checkout']
@ -1031,6 +1437,27 @@ class Project(object):
os.makedirs(self.gitdir)
self.bare_git.init()
mp = self.manifest.manifestProject
ref_dir = mp.config.GetString('repo.reference')
if ref_dir:
mirror_git = os.path.join(ref_dir, self.name + '.git')
repo_git = os.path.join(ref_dir, '.repo', 'projects',
self.relpath + '.git')
if os.path.exists(mirror_git):
ref_dir = mirror_git
elif os.path.exists(repo_git):
ref_dir = repo_git
else:
ref_dir = None
if ref_dir:
_lwrite(os.path.join(self.gitdir, 'objects/info/alternates'),
os.path.join(ref_dir, 'objects') + '\n')
if self.manifest.IsMirror:
self.config.SetString('core.bare', 'true')
else:
@ -1054,13 +1481,16 @@ class Project(object):
hooks = self._gitdir_path('hooks')
if not os.path.exists(hooks):
os.makedirs(hooks)
for stock_hook in repo_hooks():
for stock_hook in _ProjectHooks():
name = os.path.basename(stock_hook)
if name in ('commit-msg') and not self.remote.review:
if name in ('commit-msg',) and not self.remote.review \
and not self is self.manifest.manifestProject:
# Don't install a Gerrit Code Review hook if this
# project does not appear to use it for reviews.
#
# Since the manifest project is one of those, but also
# managed through gerrit, it's excluded
continue
dst = os.path.join(hooks, name)
@ -1150,6 +1580,11 @@ class Project(object):
cmd.append(HEAD)
if GitCommand(self, cmd).Wait() != 0:
raise GitError("cannot initialize work tree")
rr_cache = os.path.join(self.gitdir, 'rr-cache')
if not os.path.exists(rr_cache):
os.makedirs(rr_cache)
self._CopyFiles()
def _gitdir_path(self, path):
@ -1308,6 +1743,22 @@ class Project(object):
return r
def __getattr__(self, name):
"""Allow arbitrary git commands using pythonic syntax.
This allows you to do things like:
git_obj.rev_parse('HEAD')
Since we don't have a 'rev_parse' method defined, the __getattr__ will
run. We'll replace the '_' with a '-' and try to run a git command.
Any other arguments will be passed to the git command.
Args:
name: The name of the git command to call. Any '_' characters will
be replaced with '-'.
Returns:
A callable object that will try to call git with the named command.
"""
name = name.replace('_', '-')
def runner(*args):
cmdv = [name]

27
repo
View File

@ -2,7 +2,7 @@
## repo default configuration
##
REPO_URL='git://android.git.kernel.org/tools/repo.git'
REPO_URL='https://code.google.com/p/git-repo/'
REPO_REV='stable'
# Copyright (C) 2008 Google Inc.
@ -28,7 +28,7 @@ if __name__ == '__main__':
del magic
# increment this whenever we make important changes to this script
VERSION = (1, 8)
VERSION = (1, 12)
# increment this if the MAINTAINER_KEYS block is modified
KEYRING_VERSION = (1,0)
@ -118,6 +118,13 @@ group.add_option('-m', '--manifest-name',
group.add_option('--mirror',
dest='mirror', action='store_true',
help='mirror the forrest')
group.add_option('--reference',
dest='reference',
help='location of mirror directory', metavar='DIR')
group.add_option('--depth', type='int', default=None,
dest='depth',
help='create a shallow clone with given depth; see git clone')
# Tool
group = init_optparse.add_option_group('repo Version options')
@ -256,8 +263,8 @@ def _SetupGnuPG(quiet):
gpg_dir, e.strerror)
sys.exit(1)
env = dict(os.environ)
env['GNUPGHOME'] = gpg_dir
env = os.environ.copy()
env['GNUPGHOME'] = gpg_dir.encode()
cmd = ['gpg', '--import']
try:
@ -375,8 +382,8 @@ def _Verify(cwd, branch, quiet):
% (branch, cur)
print >>sys.stderr
env = dict(os.environ)
env['GNUPGHOME'] = gpg_dir
env = os.environ.copy()
env['GNUPGHOME'] = gpg_dir.encode()
cmd = [GIT, 'tag', '-v', cur]
proc = subprocess.Popen(cmd,
@ -427,10 +434,14 @@ def _FindRepo():
dir = os.getcwd()
repo = None
while dir != '/' and not repo:
olddir = None
while dir != '/' \
and dir != olddir \
and not repo:
repo = os.path.join(dir, repodir, REPO_MAIN)
if not os.path.isfile(repo):
repo = None
olddir = dir
dir = os.path.dirname(dir)
return (repo, os.path.join(dir, repodir))
@ -476,6 +487,7 @@ def _Help(args):
if args:
if args[0] == 'init':
init_optparse.print_help()
sys.exit(0)
else:
print >>sys.stderr,\
"error: '%s' is not a bootstrap command.\n"\
@ -588,4 +600,3 @@ def main(orig_args):
if __name__ == '__main__':
main(sys.argv[1:])

View File

@ -41,21 +41,30 @@ It is equivalent to "git branch -D <branchname>".
nb = args[0]
err = []
success = []
all = self.GetProjects(args[1:])
pm = Progress('Abandon %s' % nb, len(all))
for project in all:
pm.update()
if not project.AbandonBranch(nb):
err.append(project)
status = project.AbandonBranch(nb)
if status is not None:
if status:
success.append(project)
else:
err.append(project)
pm.end()
if err:
if len(err) == len(all):
print >>sys.stderr, 'error: no project has branch %s' % nb
else:
for p in err:
print >>sys.stderr,\
"error: %s/: cannot abandon %s" \
% (p.relpath, nb)
for p in err:
print >>sys.stderr,\
"error: %s/: cannot abandon %s" \
% (p.relpath, nb)
sys.exit(1)
elif not success:
print >>sys.stderr, 'error: no project has branch %s' % nb
sys.exit(1)
else:
print >>sys.stderr, 'Abandoned in %d project(s):\n %s' % (
len(success), '\n '.join(p.relpath for p in success))

View File

@ -136,7 +136,7 @@ is shown, then the branch appears in all projects.
hdr('%c%c %-*s' % (current, published, width, name))
out.write(' |')
if in_cnt < project_cnt and (in_cnt == 1):
if in_cnt < project_cnt:
fmt = out.write
paths = []
if in_cnt < project_cnt - in_cnt:
@ -150,15 +150,17 @@ is shown, then the branch appears in all projects.
for b in i.projects:
have.add(b.project)
for p in projects:
paths.append(p.relpath)
if not p in have:
paths.append(p.relpath)
s = ' %s %s' % (type, ', '.join(paths))
if width + 7 + len(s) < 80:
fmt(s)
else:
out.nl()
fmt(' %s:' % type)
fmt(' %s:' % type)
for p in paths:
out.nl()
fmt(' %s' % p)
fmt(width*' ' + ' %s' % p)
else:
out.write(' in all projects')
out.nl()

View File

@ -38,21 +38,27 @@ The command is equivalent to:
nb = args[0]
err = []
success = []
all = self.GetProjects(args[1:])
pm = Progress('Checkout %s' % nb, len(all))
for project in all:
pm.update()
if not project.CheckoutBranch(nb):
err.append(project)
status = project.CheckoutBranch(nb)
if status is not None:
if status:
success.append(project)
else:
err.append(project)
pm.end()
if err:
if len(err) == len(all):
print >>sys.stderr, 'error: no project has branch %s' % nb
else:
for p in err:
print >>sys.stderr,\
"error: %s/: cannot checkout %s" \
% (p.relpath, nb)
for p in err:
print >>sys.stderr,\
"error: %s/: cannot checkout %s" \
% (p.relpath, nb)
sys.exit(1)
elif not success:
print >>sys.stderr, 'error: no project has branch %s' % nb
sys.exit(1)

114
subcmds/cherry_pick.py Normal file
View File

@ -0,0 +1,114 @@
#
# Copyright (C) 2010 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 sys, re, string, random, os
from command import Command
from git_command import GitCommand
CHANGE_ID_RE = re.compile(r'^\s*Change-Id: I([0-9a-f]{40})\s*$')
class CherryPick(Command):
common = True
helpSummary = "Cherry-pick a change."
helpUsage = """
%prog <sha1>
"""
helpDescription = """
'%prog' cherry-picks a change from one branch to another.
The change id will be updated, and a reference to the old
change id will be added.
"""
def _Options(self, p):
pass
def Execute(self, opt, args):
if len(args) != 1:
self.Usage()
reference = args[0]
p = GitCommand(None,
['rev-parse', '--verify', reference],
capture_stdout = True,
capture_stderr = True)
if p.Wait() != 0:
print >>sys.stderr, p.stderr
sys.exit(1)
sha1 = p.stdout.strip()
p = GitCommand(None, ['cat-file', 'commit', sha1], capture_stdout=True)
if p.Wait() != 0:
print >>sys.stderr, "error: Failed to retrieve old commit message"
sys.exit(1)
old_msg = self._StripHeader(p.stdout)
p = GitCommand(None,
['cherry-pick', sha1],
capture_stdout = True,
capture_stderr = True)
status = p.Wait()
print >>sys.stdout, p.stdout
print >>sys.stderr, p.stderr
if status == 0:
# The cherry-pick was applied correctly. We just need to edit the
# commit message.
new_msg = self._Reformat(old_msg, sha1)
p = GitCommand(None, ['commit', '--amend', '-F', '-'],
provide_stdin = True,
capture_stdout = True,
capture_stderr = True)
p.stdin.write(new_msg)
if p.Wait() != 0:
print >>sys.stderr, "error: Failed to update commit message"
sys.exit(1)
else:
print >>sys.stderr, """\
NOTE: When committing (please see above) and editing the commit message,
please remove the old Change-Id-line and add:
"""
print >>sys.stderr, self._GetReference(sha1)
print >>sys.stderr
def _IsChangeId(self, line):
return CHANGE_ID_RE.match(line)
def _GetReference(self, sha1):
return "(cherry picked from commit %s)" % sha1
def _StripHeader(self, commit_msg):
lines = commit_msg.splitlines()
return "\n".join(lines[lines.index("")+1:])
def _Reformat(self, old_msg, sha1):
new_msg = []
for line in old_msg.splitlines():
if not self._IsChangeId(line):
new_msg.append(line)
# Add a blank line between the message and the change id/reference
try:
if new_msg[-1].strip() != "":
new_msg.append("")
except IndexError:
pass
new_msg.append(self._GetReference(sha1))
return "\n".join(new_msg)

View File

@ -36,6 +36,9 @@ makes it available in your project's local working directory.
pass
def _ParseChangeIds(self, args):
if not args:
self.Usage()
to_get = []
project = None

View File

@ -151,11 +151,11 @@ terminal and are not redirected.
first = True
for project in self.GetProjects(args):
env = dict(os.environ.iteritems())
env = os.environ.copy()
def setenv(name, val):
if val is None:
val = ''
env[name] = val
env[name] = val.encode()
setenv('REPO_PROJECT', project.name)
setenv('REPO_PATH', project.relpath)
@ -169,6 +169,12 @@ terminal and are not redirected.
else:
cwd = project.worktree
if not os.path.exists(cwd):
if (opt.project_header and opt.verbose) \
or not opt.project_header:
print >>sys.stderr, 'skipping %s/' % project.relpath
continue
if opt.project_header:
stdin = subprocess.PIPE
stdout = subprocess.PIPE

View File

@ -204,7 +204,7 @@ contain a line that matches both expressions:
else:
out.project('--- project %s ---' % project.relpath)
out.nl()
out.write(p.stderr)
out.write("%s", p.stderr)
out.nl()
continue
have_match = True
@ -217,17 +217,17 @@ contain a line that matches both expressions:
if have_rev and full_name:
for line in r:
rev, line = line.split(':', 1)
out.write(rev)
out.write("%s", rev)
out.write(':')
out.project(project.relpath)
out.write('/')
out.write(line)
out.write("%s", line)
out.nl()
elif full_name:
for line in r:
out.project(project.relpath)
out.write('/')
out.write(line)
out.write("%s", line)
out.nl()
else:
for line in r:

View File

@ -94,6 +94,8 @@ See 'repo help --all' for a complete list of recognized commands.
body = getattr(cmd, bodyAttr)
except AttributeError:
return
if body == '' or body is None:
return
self.nl()

View File

@ -14,6 +14,7 @@
# limitations under the License.
import os
import shutil
import sys
from color import Coloring
@ -41,6 +42,13 @@ The optional -m argument can be used to specify an alternate manifest
to be used. If no manifest is specified, the manifest default.xml
will be used.
The --reference option can be used to point to a directory that
has the content of a --mirror sync. This will make the working
directory use as much data as possible from the local reference
directory when fetching from the server. This will make the sync
go a lot faster by reducing data traffic on the network.
Switching Manifest Branches
---------------------------
@ -71,7 +79,12 @@ to update the working directory files.
g.add_option('--mirror',
dest='mirror', action='store_true',
help='mirror the forrest')
g.add_option('--reference',
dest='reference',
help='location of mirror directory', metavar='DIR')
g.add_option('--depth', type='int', default=None,
dest='depth',
help='create a shallow clone with given depth; see git clone')
# Tool
g = p.add_option_group('repo Version options')
@ -115,6 +128,9 @@ to update the working directory files.
r.ResetFetch()
r.Save()
if opt.reference:
m.config.SetString('repo.reference', opt.reference)
if opt.mirror:
if is_new:
m.config.SetString('repo.mirror', 'true')
@ -125,6 +141,11 @@ to update the working directory files.
if not m.Sync_NetworkHalf():
r = m.GetRemote(m.remote.name)
print >>sys.stderr, 'fatal: cannot obtain manifest %s' % r.url
# 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.
if is_new:
shutil.rmtree(m.gitdir)
sys.exit(1)
syncbuf = SyncBuffer(m.config)
@ -167,8 +188,9 @@ to update the working directory files.
print ''
print 'Your identity is: %s <%s>' % (name, email)
sys.stdout.write('is this correct [yes/no]? ')
if 'yes' == sys.stdin.readline().strip():
sys.stdout.write('is this correct [y/n]? ')
a = sys.stdin.readline().strip()
if a in ('yes', 'y', 't', 'true'):
break
if name != mp.UserName:
@ -213,6 +235,25 @@ to update the working directory files.
if a in ('y', 'yes', 't', 'true', 'on'):
gc.SetString('color.ui', 'auto')
def _ConfigureDepth(self, opt):
"""Configure the depth we'll sync down.
Args:
opt: Options from optparse. We care about opt.depth.
"""
# Opt.depth will be non-None if user actually passed --depth to repo init.
if opt.depth is not None:
if opt.depth > 0:
# Positive values will set the depth.
depth = str(opt.depth)
else:
# Negative numbers will clear the depth; passing None to SetString
# will do that.
depth = None
# We store the depth in the main manifest project.
self.manifest.manifestProject.config.SetString('repo.depth', depth)
def Execute(self, opt, args):
git_require(MIN_GIT_VERSION, fail=True)
self._SyncManifest(opt)
@ -222,6 +263,8 @@ to update the working directory files.
self._ConfigureUser()
self._ConfigureColor()
self._ConfigureDepth(opt)
if self.manifest.IsMirror:
type = 'mirror '
else:

48
subcmds/list.py Normal file
View File

@ -0,0 +1,48 @@
#
# Copyright (C) 2011 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 command import Command, MirrorSafeCommand
class List(Command, MirrorSafeCommand):
common = True
helpSummary = "List projects and their associated directories"
helpUsage = """
%prog [<project>...]
"""
helpDescription = """
List all projects; pass '.' to list the project for the cwd.
This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
"""
def Execute(self, opt, args):
"""List all projects and the associated directories.
This may be possible to do with 'repo forall', but repo newbies have
trouble figuring that out. The idea here is that it should be more
discoverable.
Args:
opt: The options. We don't take any.
args: Positional args. Can be a list of projects to list, or empty.
"""
projects = self.GetProjects(args)
lines = []
for project in projects:
lines.append("%s : %s" % (project.relpath, project.name))
lines.sort()
print '\n'.join(lines)

107
subcmds/rebase.py Normal file
View File

@ -0,0 +1,107 @@
#
# Copyright (C) 2010 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 sys
from command import Command
from git_command import GitCommand
from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M
from error import GitError
class Rebase(Command):
common = True
helpSummary = "Rebase local branches on upstream branch"
helpUsage = """
%prog {[<project>...] | -i <project>...}
"""
helpDescription = """
'%prog' uses git rebase to move local changes in the current topic branch to
the HEAD of the upstream history, useful when you have made commits in a topic
branch but need to incorporate new upstream changes "underneath" them.
"""
def _Options(self, p):
p.add_option('-i', '--interactive',
dest="interactive", action="store_true",
help="interactive rebase (single project only)")
p.add_option('-f', '--force-rebase',
dest='force_rebase', action='store_true',
help='Pass --force-rebase to git rebase')
p.add_option('--no-ff',
dest='no_ff', action='store_true',
help='Pass --no-ff to git rebase')
p.add_option('-q', '--quiet',
dest='quiet', action='store_true',
help='Pass --quiet to git rebase')
p.add_option('--autosquash',
dest='autosquash', action='store_true',
help='Pass --autosquash to git rebase')
p.add_option('--whitespace',
dest='whitespace', action='store', metavar='WS',
help='Pass --whitespace to git rebase')
def Execute(self, opt, args):
all = self.GetProjects(args)
one_project = len(all) == 1
if opt.interactive and not one_project:
print >>sys.stderr, 'error: interactive rebase not supported with multiple projects'
return -1
for project in all:
cb = project.CurrentBranch
if not cb:
if one_project:
print >>sys.stderr, "error: project %s has a detatched HEAD" % project.relpath
return -1
# ignore branches with detatched HEADs
continue
upbranch = project.GetBranch(cb)
if not upbranch.LocalMerge:
if one_project:
print >>sys.stderr, "error: project %s does not track any remote branches" % project.relpath
return -1
# ignore branches without remotes
continue
args = ["rebase"]
if opt.whitespace:
args.append('--whitespace=%s' % opt.whitespace)
if opt.quiet:
args.append('--quiet')
if opt.force_rebase:
args.append('--force-rebase')
if opt.no_ff:
args.append('--no-ff')
if opt.autosquash:
args.append('--autosquash')
if opt.interactive:
args.append("-i")
args.append(upbranch.LocalMerge)
print >>sys.stderr, '# %s: rebasing %s -> %s' % \
(project.relpath, cb, upbranch.LocalMerge)
if GitCommand(project, args).Wait() != 0:
return -1

View File

@ -55,6 +55,7 @@ need to be performed by an end-user.
print >>sys.stderr, "error: can't update repo"
sys.exit(1)
rp.bare_git.gc('--auto')
_PostRepoFetch(rp,
no_repo_verify = opt.no_repo_verify,
verbose = True)

33
subcmds/smartsync.py Normal file
View File

@ -0,0 +1,33 @@
#
# Copyright (C) 2010 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 sync import Sync
class Smartsync(Sync):
common = True
helpSummary = "Update working tree to the latest known good revision"
helpUsage = """
%prog [<project>...]
"""
helpDescription = """
The '%prog' command is a shortcut for sync -s.
"""
def _Options(self, p):
Sync._Options(self, p, show_smart=False)
def Execute(self, opt, args):
opt.smart_sync = True
Sync.Execute(self, opt, args)

View File

@ -15,6 +15,15 @@
from command import PagedCommand
try:
import threading as _threading
except ImportError:
import dummy_threading as _threading
import itertools
import sys
import StringIO
class Status(PagedCommand):
common = True
helpSummary = "Show the working tree status"
@ -27,6 +36,9 @@ and the most recent commit on this branch (HEAD), in each project
specified. A summary is displayed, one line per file where there
is a difference between these three states.
The -j/--jobs option can be used to run multiple status queries
in parallel.
Status Display
--------------
@ -60,9 +72,34 @@ the following meanings:
"""
def _Options(self, p):
p.add_option('-j', '--jobs',
dest='jobs', action='store', type='int', default=2,
help="number of projects to check simultaneously")
def _StatusHelper(self, project, clean_counter, sem, output):
"""Obtains the status for a specific project.
Obtains the status for a project, redirecting the output to
the specified object. It will release the semaphore
when done.
Args:
project: Project to get status of.
clean_counter: Counter for clean projects.
sem: Semaphore, will call release() when complete.
output: Where to output the status.
"""
try:
state = project.PrintWorkTreeStatus(output)
if state == 'CLEAN':
clean_counter.next()
finally:
sem.release()
def Execute(self, opt, args):
all = self.GetProjects(args)
clean = 0
counter = itertools.count()
on = {}
for project in all:
@ -77,9 +114,24 @@ the following meanings:
for cb in branch_names:
print '# on branch %s' % cb
for project in all:
state = project.PrintWorkTreeStatus()
if state == 'CLEAN':
clean += 1
if len(all) == clean:
if opt.jobs == 1:
for project in all:
state = project.PrintWorkTreeStatus()
if state == 'CLEAN':
counter.next()
else:
sem = _threading.Semaphore(opt.jobs)
threads_and_output = []
for project in all:
sem.acquire()
output = StringIO.StringIO()
t = _threading.Thread(target=self._StatusHelper,
args=(project, counter, sem, output))
threads_and_output.append((t, output))
t.start()
for (t, output) in threads_and_output:
t.join()
sys.stdout.write(output.getvalue())
output.close()
if len(all) == counter.next():
print 'nothing to commit (working directory clean)'

View File

@ -17,11 +17,27 @@ from optparse import SUPPRESS_HELP
import os
import re
import shutil
import socket
import subprocess
import sys
import time
import xmlrpclib
try:
import threading as _threading
except ImportError:
import dummy_threading as _threading
try:
import resource
def _rlimit_nofile():
return resource.getrlimit(resource.RLIMIT_NOFILE)
except ImportError:
def _rlimit_nofile():
return (256, 256)
from git_command import GIT
from git_refs import R_HEADS
from project import HEAD
from project import Project
from project import RemoteSpec
@ -31,7 +47,12 @@ from project import R_HEADS
from project import SyncBuffer
from progress import Progress
class _FetchError(Exception):
"""Internal error thrown in _FetchHelper() when we don't want stack trace."""
pass
class Sync(Command, MirrorSafeCommand):
jobs = 1
common = True
helpSummary = "Update working tree to the latest revision"
helpUsage = """
@ -57,6 +78,14 @@ back to the manifest revision. This option is especially helpful
if the project is currently on a topic branch, but the manifest
revision is temporarily needed.
The -s/--smart-sync option can be used to sync to a known good
build as specified by the manifest-server element in the current
manifest. The -t/--smart-tag option is similar and allows you to
specify a custom tag/label.
The -f/--force-broken option can be used to proceed with syncing
other projects if a project sync fails.
SSH Connections
---------------
@ -87,7 +116,12 @@ later is required to fix a server side protocol bug.
"""
def _Options(self, p):
def _Options(self, p, show_smart=True):
self.jobs = self.manifest.default.sync_j
p.add_option('-f', '--force-broken',
dest='force_broken', action='store_true',
help="continue sync even if a project fails to sync")
p.add_option('-l','--local-only',
dest='local_only', action='store_true',
help="only update working tree, don't fetch")
@ -97,6 +131,20 @@ later is required to fix a server side protocol bug.
p.add_option('-d','--detach',
dest='detach_head', action='store_true',
help='detach projects back to manifest revision')
p.add_option('-q','--quiet',
dest='quiet', action='store_true',
help='be more quiet')
p.add_option('-j','--jobs',
dest='jobs', action='store', type='int',
default=self.jobs,
help="projects to fetch simultaneously (default %d)" % self.jobs)
if show_smart:
p.add_option('-s', '--smart-sync',
dest='smart_sync', action='store_true',
help='smart sync using manifest from a known good build')
p.add_option('-t', '--smart-tag',
dest='smart_tag', action='store',
help='smart sync using manifest from a known tag')
g = p.add_option_group('repo Version options')
g.add_option('--no-repo-verify',
@ -106,17 +154,111 @@ later is required to fix a server side protocol bug.
dest='repo_upgraded', action='store_true',
help=SUPPRESS_HELP)
def _Fetch(self, projects):
def _FetchHelper(self, opt, project, lock, fetched, pm, sem, err_event):
"""Main function of the fetch threads when jobs are > 1.
Args:
opt: Program options returned from optparse. See _Options().
project: Project object for the project to fetch.
lock: Lock for accessing objects that are shared amongst multiple
_FetchHelper() threads.
fetched: set object that we will add project.gitdir to when we're done
(with our lock held).
pm: Instance of a Project object. We will call pm.update() (with our
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
out info about the error).
"""
# We'll set to true once we've locked the lock.
did_lock = False
# Encapsulate everything in a try/except/finally so that:
# - We always set err_event in the case of an exception.
# - We always make sure we call sem.release().
# - We always make sure we unlock the lock if we locked it.
try:
try:
success = project.Sync_NetworkHalf(quiet=opt.quiet)
# 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:
print >>sys.stderr, 'error: Cannot fetch %s' % project.name
if opt.force_broken:
print >>sys.stderr, 'warn: --force-broken, continuing to sync'
else:
raise _FetchError()
fetched.add(project.gitdir)
pm.update()
except BaseException, e:
# Notify the _Fetch() function about all errors.
err_event.set()
# If we got our own _FetchError, we don't want a stack trace.
# However, if we got something else (something in Sync_NetworkHalf?),
# we'd like one (so re-raise after we've set err_event).
if not isinstance(e, _FetchError):
raise
finally:
if did_lock:
lock.release()
sem.release()
def _Fetch(self, projects, opt):
fetched = set()
pm = Progress('Fetching projects', len(projects))
for project in projects:
pm.update()
if project.Sync_NetworkHalf():
fetched.add(project.gitdir)
else:
print >>sys.stderr, 'error: Cannot fetch %s' % project.name
if self.jobs == 1:
for project in projects:
pm.update()
if project.Sync_NetworkHalf(quiet=opt.quiet):
fetched.add(project.gitdir)
else:
print >>sys.stderr, 'error: Cannot fetch %s' % project.name
if opt.force_broken:
print >>sys.stderr, 'warn: --force-broken, continuing to sync'
else:
sys.exit(1)
else:
threads = set()
lock = _threading.Lock()
sem = _threading.Semaphore(self.jobs)
err_event = _threading.Event()
for project in projects:
# Check for any errors before starting any new threads.
# ...we'll let existing threads finish, though.
if err_event.isSet():
break
sem.acquire()
t = _threading.Thread(target = self._FetchHelper,
args = (opt,
project,
lock,
fetched,
pm,
sem,
err_event))
threads.add(t)
t.start()
for t in threads:
t.join()
# If we saw an error, exit with code 1 so that other scripts can check.
if err_event.isSet():
print >>sys.stderr, '\nerror: Exited sync due to fetch errors'
sys.exit(1)
pm.end()
for project in projects:
project.bare_git.gc('--auto')
return fetched
def UpdateProjectList(self):
@ -138,32 +280,36 @@ later is required to fix a server side protocol bug.
if not path:
continue
if path not in new_project_paths:
project = Project(
manifest = self.manifest,
name = path,
remote = RemoteSpec('origin'),
gitdir = os.path.join(self.manifest.topdir,
path, '.git'),
worktree = os.path.join(self.manifest.topdir, path),
relpath = path,
revisionExpr = 'HEAD',
revisionId = None)
if project.IsDirty():
print >>sys.stderr, 'error: Cannot remove project "%s": \
"""If the path has already been deleted, we don't need to do it
"""
if os.path.exists(self.manifest.topdir + '/' + path):
project = Project(
manifest = self.manifest,
name = path,
remote = RemoteSpec('origin'),
gitdir = os.path.join(self.manifest.topdir,
path, '.git'),
worktree = os.path.join(self.manifest.topdir, path),
relpath = path,
revisionExpr = 'HEAD',
revisionId = None)
if project.IsDirty():
print >>sys.stderr, 'error: Cannot remove project "%s": \
uncommitted changes are present' % project.relpath
print >>sys.stderr, ' commit changes, then run sync again'
return -1
else:
print >>sys.stderr, 'Deleting obsolete path %s' % project.worktree
shutil.rmtree(project.worktree)
# Try deleting parent subdirs if they are empty
dir = os.path.dirname(project.worktree)
while dir != self.manifest.topdir:
try:
os.rmdir(dir)
except OSError:
break
dir = os.path.dirname(dir)
print >>sys.stderr, ' commit changes, then run sync again'
return -1
else:
print >>sys.stderr, 'Deleting obsolete path %s' % project.worktree
shutil.rmtree(project.worktree)
# Try deleting parent subdirs if they are empty
dir = os.path.dirname(project.worktree)
while dir != self.manifest.topdir:
try:
os.rmdir(dir)
except OSError:
break
dir = os.path.dirname(dir)
new_project_paths.sort()
fd = open(file_path, 'w')
@ -175,6 +321,12 @@ uncommitted changes are present' % project.relpath
return 0
def Execute(self, opt, args):
if opt.jobs:
self.jobs = opt.jobs
if self.jobs > 1:
soft_limit, _ = _rlimit_nofile()
self.jobs = min(self.jobs, (soft_limit - 5) / 3)
if opt.network_only and opt.detach_head:
print >>sys.stderr, 'error: cannot combine -n and -d'
sys.exit(1)
@ -182,6 +334,55 @@ uncommitted changes are present' % project.relpath
print >>sys.stderr, 'error: cannot combine -n and -l'
sys.exit(1)
if opt.smart_sync or opt.smart_tag:
if not self.manifest.manifest_server:
print >>sys.stderr, \
'error: cannot smart sync: no manifest server defined in manifest'
sys.exit(1)
try:
server = xmlrpclib.Server(self.manifest.manifest_server)
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 (env.has_key('TARGET_PRODUCT') and
env.has_key('TARGET_BUILD_VARIANT')):
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_override.xml"
manifest_path = os.path.join(self.manifest.manifestProject.worktree,
manifest_name)
try:
f = open(manifest_path, 'w')
try:
f.write(manifest_str)
finally:
f.close()
except IOError:
print >>sys.stderr, 'error: cannot write manifest to %s' % \
manifest_path
sys.exit(1)
self.manifest.Override(manifest_name)
else:
print >>sys.stderr, 'error: %s' % manifest_str
sys.exit(1)
except socket.error:
print >>sys.stderr, 'error: cannot connect to manifest server %s' % (
self.manifest.manifest_server)
sys.exit(1)
rp = self.manifest.repoProject
rp.PreSync()
@ -192,7 +393,7 @@ uncommitted changes are present' % project.relpath
_PostRepoUpgrade(self.manifest)
if not opt.local_only:
mp.Sync_NetworkHalf()
mp.Sync_NetworkHalf(quiet=opt.quiet)
if mp.HasChanges:
syncbuf = SyncBuffer(mp.config)
@ -209,7 +410,7 @@ uncommitted changes are present' % project.relpath
to_fetch.append(rp)
to_fetch.extend(all)
fetched = self._Fetch(to_fetch)
fetched = self._Fetch(to_fetch, opt)
_PostRepoFetch(rp, opt.no_repo_verify)
if opt.network_only:
# bail out now; the rest touches the working tree
@ -221,7 +422,7 @@ uncommitted changes are present' % project.relpath
for project in all:
if project.gitdir not in fetched:
missing.append(project)
self._Fetch(missing)
self._Fetch(missing, opt)
if self.manifest.IsMirror:
# bail out now, we have no working tree
@ -242,6 +443,11 @@ uncommitted changes are present' % project.relpath
if not syncbuf.Finish():
sys.exit(1)
# If there's a notice that's supposed to print at the end of the sync, print
# it now...
if self.manifest.notice:
print self.manifest.notice
def _PostRepoUpgrade(manifest):
for project in manifest.projects.values():
if project.Exists:
@ -289,9 +495,9 @@ warning: Cannot automatically authenticate repo."""
% (project.name, rev)
return False
env = dict(os.environ)
env['GIT_DIR'] = project.gitdir
env['GNUPGHOME'] = gpg_dir
env = os.environ.copy()
env['GIT_DIR'] = project.gitdir.encode()
env['GNUPGHOME'] = gpg_dir.encode()
cmd = [GIT, 'tag', '-v', cur]
proc = subprocess.Popen(cmd,

View File

@ -13,12 +13,25 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import re
import sys
from command import InteractiveCommand
from editor import Editor
from error import UploadError
from error import HookError, UploadError
from project import RepoHook
UNUSUAL_COMMIT_THRESHOLD = 5
def _ConfirmManyUploads(multiple_branches=False):
if multiple_branches:
print "ATTENTION: One or more branches has an unusually high number of commits."
else:
print "ATTENTION: You are uploading an unusually high number of commits."
print "YOU PROBABLY DO NOT MEAN TO DO THIS. (Did you rebase across branches?)"
answer = raw_input("If you are sure you intend to do this, type 'yes': ").strip()
return answer == "yes"
def _die(fmt, *args):
msg = fmt % args
@ -35,7 +48,7 @@ class Upload(InteractiveCommand):
common = True
helpSummary = "Upload changes for code review"
helpUsage="""
%prog [--re --cc] {[<project>]... | --replace <project>}
%prog [--re --cc] [<project>]...
"""
helpDescription = """
The '%prog' command is used to send changes to the Gerrit Code
@ -55,12 +68,6 @@ added to the respective list of users, and emails are sent to any
new users. Users passed as --reviewers must already be registered
with the code review system, or the upload will fail.
If the --replace option is passed the user can designate which
existing change(s) in Gerrit match up to the commits in the branch
being uploaded. For each matched pair of change,commit the commit
will be added as a new patch set, completely replacing the set of
files and description associated with the change in Gerrit.
Configuration
-------------
@ -72,6 +79,19 @@ to "true" then repo will assume you always answer "y" at the prompt,
and will not prompt you further. If it is set to "false" then repo
will assume you always answer "n", and will abort.
review.URL.autocopy:
To automatically copy a user or mailing list to all uploaded reviews,
you can set a per-project or global Git option to do so. Specifically,
review.URL.autocopy can be set to a comma separated list of reviewers
who you always want copied on all uploads with a non-empty --re
argument.
review.URL.username:
Override the username used to connect to Gerrit Code Review.
By default the local part of the email address is used.
The URL must match the review URL listed in the manifest XML file,
or in the .git/config within the project. For example:
@ -81,6 +101,7 @@ or in the .git/config within the project. For example:
[review "http://review.example.com/"]
autoupload = true
autocopy = johndoe@company.com,my-team-alias@company.com
References
----------
@ -90,17 +111,43 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
"""
def _Options(self, p):
p.add_option('--replace',
dest='replace', action='store_true',
help='Upload replacement patchesets from this branch')
p.add_option('-t',
dest='auto_topic', action='store_true',
help='Send local branch name to Gerrit Code Review')
p.add_option('--re', '--reviewers',
type='string', action='append', dest='reviewers',
help='Request reviews from these people.')
p.add_option('--cc',
type='string', action='append', dest='cc',
help='Also send email to these email addresses.')
p.add_option('--br',
type='string', action='store', dest='branch',
help='Branch to upload.')
def _SingleBranch(self, branch, people):
# Options relating to upload hook. Note that verify and no-verify are NOT
# opposites of each other, which is why they store to different locations.
# We are using them to match 'git commit' syntax.
#
# Combinations:
# - no-verify=False, verify=False (DEFAULT):
# If stdout is a tty, can prompt about running upload hooks if needed.
# If user denies running hooks, the upload is cancelled. If stdout is
# not a tty and we would need to prompt about upload hooks, upload is
# cancelled.
# - no-verify=False, verify=True:
# Always run upload hooks with no prompt.
# - no-verify=True, verify=False:
# Never run upload hooks, but upload anyway (AKA bypass hooks).
# - no-verify=True, verify=True:
# Invalid
p.add_option('--no-verify',
dest='bypass_hooks', action='store_true',
help='Do not run the upload hook.')
p.add_option('--verify',
dest='allow_all_hooks', action='store_true',
help='Run the upload hook without prompting.')
def _SingleBranch(self, opt, branch, people):
project = branch.project
name = branch.name
remote = project.GetBranch(name).remote
@ -129,11 +176,15 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
answer = answer in ('y', 'Y', 'yes', '1', 'true', 't')
if answer:
self._UploadAndReport([branch], people)
if len(branch.commits) > UNUSUAL_COMMIT_THRESHOLD:
answer = _ConfirmManyUploads()
if answer:
self._UploadAndReport(opt, [branch], people)
else:
_die("upload aborted by user")
def _MultipleBranches(self, pending, people):
def _MultipleBranches(self, opt, pending, people):
projects = {}
branches = {}
@ -192,7 +243,30 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
todo.append(branch)
if not todo:
_die("nothing uncommented for upload")
self._UploadAndReport(todo, people)
many_commits = False
for branch in todo:
if len(branch.commits) > UNUSUAL_COMMIT_THRESHOLD:
many_commits = True
break
if many_commits:
if not _ConfirmManyUploads(multiple_branches=True):
_die("upload aborted by user")
self._UploadAndReport(opt, todo, people)
def _AppendAutoCcList(self, branch, people):
"""
Appends the list of users in the CC list in the git project's config if a
non-empty reviewer list was found.
"""
name = branch.name
project = branch.project
key = 'review.%s.autocopy' % project.GetBranch(name).remote.review
raw_list = project.config.GetString(key)
if not raw_list is None and len(people[0]) > 0:
people[1].extend([entry.strip() for entry in raw_list.split(',')])
def _FindGerritChange(self, branch):
last_pub = branch.project.WasPublished(branch.name)
@ -206,66 +280,29 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
except:
return ""
def _ReplaceBranch(self, project, people):
branch = project.CurrentBranch
if not branch:
print >>sys.stdout, "no branches ready for upload"
return
branch = project.GetUploadableBranch(branch)
if not branch:
print >>sys.stdout, "no branches ready for upload"
return
script = []
script.append('# Replacing from branch %s' % branch.name)
if len(branch.commits) == 1:
change = self._FindGerritChange(branch)
script.append('[%-6s] %s' % (change, branch.commits[0]))
else:
for commit in branch.commits:
script.append('[ ] %s' % commit)
script.append('')
script.append('# Insert change numbers in the brackets to add a new patch set.')
script.append('# To create a new change record, leave the brackets empty.')
script = Editor.EditString("\n".join(script)).split("\n")
change_re = re.compile(r'^\[\s*(\d{1,})\s*\]\s*([0-9a-f]{1,}) .*$')
to_replace = dict()
full_hashes = branch.unabbrev_commits
for line in script:
m = change_re.match(line)
if m:
c = m.group(1)
f = m.group(2)
try:
f = full_hashes[f]
except KeyError:
print 'fh = %s' % full_hashes
print >>sys.stderr, "error: commit %s not found" % f
sys.exit(1)
if c in to_replace:
print >>sys.stderr,\
"error: change %s cannot accept multiple commits" % c
sys.exit(1)
to_replace[c] = f
if not to_replace:
print >>sys.stderr, "error: no replacements specified"
print >>sys.stderr, " use 'repo upload' without --replace"
sys.exit(1)
branch.replace_changes = to_replace
self._UploadAndReport([branch], people)
def _UploadAndReport(self, todo, people):
def _UploadAndReport(self, opt, todo, original_people):
have_errors = False
for branch in todo:
try:
branch.UploadForReview(people)
people = copy.deepcopy(original_people)
self._AppendAutoCcList(branch, people)
# Check if there are local changes that may have been forgotten
if branch.project.HasChanges():
key = 'review.%s.autoupload' % branch.project.remote.review
answer = branch.project.config.GetBoolean(key)
# if they want to auto upload, let's not ask because it could be automated
if answer is None:
sys.stdout.write('Uncommitted changes in ' + branch.project.name + ' (did you forget to amend?). Continue uploading? (y/n) ')
a = sys.stdin.readline().strip().lower()
if a not in ('y', 'yes', 't', 'true', 'on'):
print >>sys.stderr, "skipping upload"
branch.uploaded = False
branch.error = 'User aborted'
continue
branch.UploadForReview(people, auto_topic=opt.auto_topic)
branch.uploaded = True
except UploadError, e:
branch.error = e
@ -273,15 +310,19 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
have_errors = True
print >>sys.stderr, ''
print >>sys.stderr, '--------------------------------------------'
print >>sys.stderr, '----------------------------------------------------------------------'
if have_errors:
for branch in todo:
if not branch.uploaded:
print >>sys.stderr, '[FAILED] %-15s %-15s (%s)' % (
if len(str(branch.error)) <= 30:
fmt = ' (%s)'
else:
fmt = '\n (%s)'
print >>sys.stderr, ('[FAILED] %-15s %-15s' + fmt) % (
branch.project.relpath + '/', \
branch.name, \
branch.error)
str(branch.error))
print >>sys.stderr, ''
for branch in todo:
@ -298,6 +339,25 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
pending = []
reviewers = []
cc = []
branch = None
if opt.branch:
branch = opt.branch
for project in project_list:
avail = project.GetUploadableBranches(branch)
if avail:
pending.append((project, avail))
if pending and (not opt.bypass_hooks):
hook = RepoHook('pre-upload', self.manifest.repo_hooks_project,
self.manifest.topdir, abort_if_user_denies=True)
pending_proj_names = [project.name for (project, avail) in pending]
try:
hook.Run(opt.allow_all_hooks, project_list=pending_proj_names)
except HookError, e:
print >>sys.stderr, "ERROR: %s" % str(e)
return
if opt.reviewers:
reviewers = _SplitEmails(opt.reviewers)
@ -305,22 +365,9 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
cc = _SplitEmails(opt.cc)
people = (reviewers,cc)
if opt.replace:
if len(project_list) != 1:
print >>sys.stderr, \
'error: --replace requires exactly one project'
sys.exit(1)
self._ReplaceBranch(project_list[0], people)
return
for project in project_list:
avail = project.GetUploadableBranches()
if avail:
pending.append((project, avail))
if not pending:
print >>sys.stdout, "no branches ready for upload"
elif len(pending) == 1 and len(pending[0][1]) == 1:
self._SingleBranch(pending[0][1][0], people)
self._SingleBranch(opt, pending[0][1][0], people)
else:
self._MultipleBranches(pending, people)
self._MultipleBranches(opt, pending, people)