Compare commits

...

23 Commits

Author SHA1 Message Date
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
13 changed files with 550 additions and 151 deletions

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

@ -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,6 +30,7 @@ LAST_CWD = None
_ssh_proxy_path = None
_ssh_sock_path = None
_ssh_clients = []
def ssh_sock(create=True):
global _ssh_sock_path
@ -51,6 +53,24 @@ 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 = []
class _GitCall(object):
def version(self):
@ -188,6 +208,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 +233,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

@ -23,7 +23,10 @@ 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/'
@ -254,8 +257,10 @@ class GitConfig(object):
finally:
fd.close()
except IOError:
if os.path.exists(self._pickle):
os.remove(self._pickle)
except cPickle.PickleError:
if os.path.exists(self._pickle):
os.remove(self._pickle)
def _ReadGit(self):
@ -353,14 +358,21 @@ class RefSpec(object):
return s
_ssh_cache = {}
_master_processes = []
_master_keys = set()
_ssh_master = True
def _open_ssh(host, port):
def _open_ssh(host, port=None):
global _ssh_master
# 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)
if key in _ssh_cache:
else:
key = host
if key in _master_keys:
return True
if not _ssh_master \
@ -370,12 +382,39 @@ def _open_ssh(host, port):
#
return False
command = ['ssh',
# We will make two calls to ssh; this is the common part of both calls.
command_base = ['ssh',
'-o','ControlPath %s' % ssh_sock(),
'-p',str(port),
'-M',
'-N',
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)
@ -386,18 +425,22 @@ def _open_ssh(host, port):
% (host,port, str(e))
return False
_ssh_cache[key] = p
_master_processes.append(p)
_master_keys.add(key)
time.sleep(1)
return True
def close_ssh():
for key,p in _ssh_cache.iteritems():
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)
if d:
@ -417,7 +460,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 +468,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
@ -519,8 +562,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" "$@"

View File

@ -13,10 +13,13 @@
# 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):
self._title = title
@ -29,7 +32,7 @@ class Progress(object):
def update(self, inc=1):
self._done += inc
if IsTrace():
if _NOT_TTY or IsTrace():
return
if not self._show:
@ -56,7 +59,7 @@ class Progress(object):
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:

View File

@ -111,7 +111,6 @@ class ReviewableBranch(object):
self.project = project
self.branch = branch
self.base = base
self.replace_changes = None
@property
def name(self):
@ -149,10 +148,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 +202,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]
@ -364,6 +367,27 @@ class Project(object):
## Status Display ##
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):
"""Prints the status of the repository to stdout.
"""
@ -530,7 +554,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 +588,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 +612,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:
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:
@ -978,7 +1020,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,15 +1030,85 @@ 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']
if quiet:
cmd.append('--quiet')
if not self.worktree:
cmd.append('--update-head-ok')
cmd.append(name)
return GitCommand(self,
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']
if quiet:
@ -1031,6 +1145,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:

5
repo
View File

@ -28,7 +28,7 @@ if __name__ == '__main__':
del magic
# increment this whenever we make important changes to this script
VERSION = (1, 8)
VERSION = (1, 9)
# increment this if the MAINTAINER_KEYS block is modified
KEYRING_VERSION = (1,0)
@ -118,6 +118,9 @@ 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')
# Tool
group = init_optparse.add_option_group('repo Version options')

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

@ -41,6 +41,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 +78,9 @@ 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')
# Tool
g = p.add_option_group('repo Version options')
@ -115,6 +124,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')

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

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

@ -23,6 +23,11 @@ import sys
import time
import xmlrpclib
try:
import threading as _threading
except ImportError:
import dummy_threading as _threading
from git_command import GIT
from git_refs import R_HEADS
from project import HEAD
@ -35,6 +40,7 @@ from project import SyncBuffer
from progress import Progress
class Sync(Command, MirrorSafeCommand):
jobs = 1
common = True
helpSummary = "Update working tree to the latest revision"
helpUsage = """
@ -64,6 +70,9 @@ 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 -f/--force-broken option can be used to proceed with syncing
other projects if a project sync fails.
SSH Connections
---------------
@ -94,7 +103,10 @@ later is required to fix a server side protocol bug.
"""
def _Options(self, p):
def _Options(self, p, show_smart=True):
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")
@ -104,6 +116,13 @@ 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',
help="number of projects to fetch simultaneously")
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')
@ -116,16 +135,55 @@ 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):
if not project.Sync_NetworkHalf(quiet=opt.quiet):
print >>sys.stderr, 'error: Cannot fetch %s' % project.name
if opt.force_broken:
print >>sys.stderr, 'warn: --force-broken, continuing to sync'
else:
sem.release()
sys.exit(1)
lock.acquire()
fetched.add(project.gitdir)
pm.update()
lock.release()
sem.release()
def _Fetch(self, projects, opt):
fetched = set()
pm = Progress('Fetching projects', len(projects))
if self.jobs == 1:
for project in projects:
pm.update()
if project.Sync_NetworkHalf():
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)
for project in projects:
sem.acquire()
t = _threading.Thread(target = self._FetchHelper,
args = (opt,
project,
lock,
fetched,
pm,
sem))
threads.add(t)
t.start()
for t in threads:
t.join()
pm.end()
return fetched
@ -189,6 +247,8 @@ uncommitted changes are present' % project.relpath
return 0
def Execute(self, opt, args):
if opt.jobs:
self.jobs = opt.jobs
if opt.network_only and opt.detach_head:
print >>sys.stderr, 'error: cannot combine -n and -d'
sys.exit(1)
@ -251,7 +311,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)
@ -268,7 +328,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
@ -280,7 +340,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

View File

@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import re
import sys
@ -46,7 +47,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
@ -66,12 +67,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
-------------
@ -83,6 +78,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:
@ -92,6 +100,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
----------
@ -101,9 +110,9 @@ 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.')
@ -111,7 +120,7 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
type='string', action='append', dest='cc',
help='Also send email to these email addresses.')
def _SingleBranch(self, branch, people):
def _SingleBranch(self, opt, branch, people):
project = branch.project
name = branch.name
remote = project.GetBranch(name).remote
@ -144,11 +153,11 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
answer = _ConfirmManyUploads()
if answer:
self._UploadAndReport([branch], people)
self._UploadAndReport(opt, [branch], people)
else:
_die("upload aborted by user")
def _MultipleBranches(self, pending, people):
def _MultipleBranches(self, opt, pending, people):
projects = {}
branches = {}
@ -217,7 +226,20 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
if not _ConfirmManyUploads(multiple_branches=True):
_die("upload aborted by user")
self._UploadAndReport(todo, people)
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)
@ -231,70 +253,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)
if len(branch.commits) > UNUSUAL_COMMIT_THRESHOLD:
if not _ConfirmManyUploads(multiple_branches=True):
_die("upload aborted by user")
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
@ -334,14 +315,6 @@ 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:
@ -350,6 +323,6 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
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)