Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(125)

Unified Diff: trychange.py

Issue 184343003: Now trychange can store patches in a Git repo (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: branch -> ref Created 6 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « tests/trychange_unittest.py ('k') | no next file » | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: trychange.py
diff --git a/trychange.py b/trychange.py
index f93d9a418778fd11c5cc52ac660952941ac88297..f979e6bfd6ce116edddfa7cacf2f640f681d3d81 100755
--- a/trychange.py
+++ b/trychange.py
@@ -4,10 +4,11 @@
# found in the LICENSE file.
"""Client-side script to send a try job to the try server. It communicates to
-the try server by either writting to a svn repository or by directly connecting
-to the server by HTTP.
+the try server by either writting to a svn/git repository or by directly
+connecting to the server by HTTP.
"""
+import contextlib
import datetime
import errno
import getpass
@@ -70,6 +71,8 @@ Examples:
-f include/b.h
"""
+GIT_PATCH_DIR_BASENAME = os.path.join('git-try', 'patches-git')
+GIT_BRANCH_FILE = 'ref'
def DieWithError(message):
print >> sys.stderr, message
@@ -164,7 +167,10 @@ class SCM(object):
'port': self.GetCodeReviewSetting('TRYSERVER_HTTP_PORT'),
'host': self.GetCodeReviewSetting('TRYSERVER_HTTP_HOST'),
'svn_repo': self.GetCodeReviewSetting('TRYSERVER_SVN_URL'),
+ 'git_repo': self.GetCodeReviewSetting('TRYSERVER_GIT_URL'),
'project': self.GetCodeReviewSetting('TRYSERVER_PROJECT'),
+ # Primarily for revision=auto
+ 'revision': self.GetCodeReviewSetting('TRYSERVER_REVISION'),
'root': self.GetCodeReviewSetting('TRYSERVER_ROOT'),
'patchlevel': self.GetCodeReviewSetting('TRYSERVER_PATCHLEVEL'),
}
@@ -410,7 +416,9 @@ def _GenTSBotSpec(checkouts, change, changed_files, options):
def _ParseSendChangeOptions(bot_spec, options):
- """Parse common options passed to _SendChangeHTTP and _SendChangeSVN."""
+ """Parse common options passed to _SendChangeHTTP, _SendChangeSVN and
+ _SendChangeGit.
+ """
values = [
('user', options.user),
('name', options.name),
@@ -481,6 +489,44 @@ def _SendChangeHTTP(bot_spec, options):
raise NoTryServerAccess('%s is unaccessible. Got:\n%s' % (url, response))
+@contextlib.contextmanager
+def _TempFilename(name, contents=None):
+ """Create a temporary directory, append the specified name and yield.
+
+ In contrast to NamedTemporaryFile, does not keep the file open.
+ Deletes the file on __exit__.
+ """
+ temp_dir = tempfile.mkdtemp(prefix=name)
+ try:
+ path = os.path.join(temp_dir, name)
+ if contents:
+ with open(path, 'w') as f:
+ f.write(contents)
+ yield path
+ finally:
+ shutil.rmtree(temp_dir, True)
+
+
+@contextlib.contextmanager
+def _PrepareDescriptionAndPatchFiles(description, options):
+ """Creates temporary files with description and patch.
+
+ __enter__ called on the return value returns a tuple of patch_filename and
+ description_filename.
+
+ Args:
+ description: contents of description file.
+ options: patchset options object. Must have attributes: user,
+ name (of patch) and diff (contents of patch).
+ """
+ current_time = str(datetime.datetime.now()).replace(':', '.')
+ patch_basename = '%s.%s.%s.diff' % (Escape(options.user),
+ Escape(options.name), current_time)
+ with _TempFilename('description', description) as description_filename:
+ with _TempFilename(patch_basename, options.diff) as patch_filename:
+ yield patch_filename, description_filename
+
+
def _SendChangeSVN(bot_spec, options):
"""Send a change to the try server by committing a diff file on a subversion
server."""
@@ -497,45 +543,150 @@ def _SendChangeSVN(bot_spec, options):
if options.dry_run:
return
- # Create a temporary directory, put a uniquely named file in it with the diff
- # content and svn import that.
- temp_dir = tempfile.mkdtemp()
- temp_file = tempfile.NamedTemporaryFile()
- try:
- try:
- # Description
- temp_file.write(description)
- temp_file.flush()
-
- # Diff file
- current_time = str(datetime.datetime.now()).replace(':', '.')
- file_name = (Escape(options.user) + '.' + Escape(options.name) +
- '.%s.diff' % current_time)
- full_path = os.path.join(temp_dir, file_name)
- with open(full_path, 'wb') as f:
- f.write(options.diff)
-
- # Committing it will trigger a try job.
- if sys.platform == "cygwin":
- # Small chromium-specific issue here:
- # git-try uses /usr/bin/python on cygwin but svn.bat will be used
- # instead of /usr/bin/svn by default. That causes bad things(tm) since
- # Windows' svn.exe has no clue about cygwin paths. Hence force to use
- # the cygwin version in this particular context.
- exe = "/usr/bin/svn"
- else:
- exe = "svn"
- command = [exe, 'import', '-q', temp_dir, options.svn_repo, '--file',
- temp_file.name]
- if scm.SVN.AssertVersion("1.5")[0]:
- command.append('--no-ignore')
+ with _PrepareDescriptionAndPatchFiles(description, options) as (
+ patch_filename, description_filename):
+ if sys.platform == "cygwin":
+ # Small chromium-specific issue here:
+ # git-try uses /usr/bin/python on cygwin but svn.bat will be used
+ # instead of /usr/bin/svn by default. That causes bad things(tm) since
+ # Windows' svn.exe has no clue about cygwin paths. Hence force to use
+ # the cygwin version in this particular context.
+ exe = "/usr/bin/svn"
+ else:
+ exe = "svn"
+ patch_dir = os.path.dirname(patch_filename)
+ command = [exe, 'import', '-q', patch_dir, options.svn_repo, '--file',
+ description_filename]
+ if scm.SVN.AssertVersion("1.5")[0]:
+ command.append('--no-ignore')
+ try:
subprocess2.check_call(command)
except subprocess2.CalledProcessError, e:
raise NoTryServerAccess(str(e))
- finally:
- temp_file.close()
- shutil.rmtree(temp_dir, True)
+
+
+def _GetPatchGitRepo(git_url):
+ """Gets a path to a Git repo with patches.
+
+ Stores patches in .git/git-try/patches-git directory, a git repo. If it
+ doesn't exist yet or its origin URL is different, cleans up and clones it.
+ If it existed before, then pulls changes.
+
+ Does not support SVN repo.
+
+ Returns a path to the directory with patches.
+ """
+ git_dir = scm.GIT.GetGitDir(os.getcwd())
+ patch_dir = os.path.join(git_dir, GIT_PATCH_DIR_BASENAME)
+
+ logging.info('Looking for git repo for patches')
+ # Is there already a repo with the expected url or should we clone?
+ clone = True
+ if os.path.exists(patch_dir) and scm.GIT.IsInsideWorkTree(patch_dir):
+ existing_url = scm.GIT.Capture(
+ ['config', '--local', 'remote.origin.url'],
+ cwd=patch_dir)
+ clone = existing_url != git_url
+
+ if clone:
+ if os.path.exists(patch_dir):
+ logging.info('Cleaning up')
+ shutil.rmtree(patch_dir, True)
+ logging.info('Cloning patch repo')
+ scm.GIT.Capture(['clone', git_url, GIT_PATCH_DIR_BASENAME], cwd=git_dir)
+ email = scm.GIT.GetEmail(cwd=os.getcwd())
+ scm.GIT.Capture(['config', '--local', 'user.email', email], cwd=patch_dir)
+ else:
+ if scm.GIT.IsWorkTreeDirty(patch_dir):
+ logging.info('Work dir is dirty: hard reset!')
+ scm.GIT.Capture(['reset', '--hard'], cwd=patch_dir)
+ logging.info('Updating patch repo')
+ scm.GIT.Capture(['pull', 'origin', 'master'], cwd=patch_dir)
+
+ return os.path.abspath(patch_dir)
+
+
+def _SendChangeGit(bot_spec, options):
+ """Send a change to the try server by committing a diff file to a GIT repo"""
+ if not options.git_repo:
+ raise NoTryServerAccess('Please use the --git_repo option to specify the '
+ 'try server git repository to connect to.')
+
+ values = _ParseSendChangeOptions(bot_spec, options)
+ comment_subject = '%s.%s' % (options.user, options.name)
+ comment_body = ''.join("%s=%s\n" % (k, v) for k, v in values)
+ description = '%s\n\n%s' % (comment_subject, comment_body)
+ logging.info('Sending by GIT')
+ logging.info(description)
+ logging.info(options.git_repo)
+ logging.info(options.diff)
+ if options.dry_run:
+ return
+
+ patch_dir = _GetPatchGitRepo(options.git_repo)
+ def patch_git(*args):
+ return scm.GIT.Capture(list(args), cwd=patch_dir)
+ def add_and_commit(filename, comment_filename):
+ patch_git('add', filename)
+ patch_git('commit', '-F', comment_filename)
+
+ assert scm.GIT.IsInsideWorkTree(patch_dir)
+ assert not scm.GIT.IsWorkTreeDirty(patch_dir)
+
+ with _PrepareDescriptionAndPatchFiles(description, options) as (
+ patch_filename, description_filename):
+ logging.info('Committing patch')
+ target_branch = ('refs/patches/' +
+ os.path.basename(patch_filename).replace(' ','-'))
+ target_filename = os.path.join(patch_dir, 'patch.diff')
+ branch_file = os.path.join(patch_dir, GIT_BRANCH_FILE)
+ try:
+ # Crete a new branch and put the patch there
+ patch_git('checkout', '--orphan', target_branch)
+ patch_git('reset')
+ patch_git('clean', '-f')
+ shutil.copyfile(patch_filename, target_filename)
+ add_and_commit(target_filename, description_filename)
+ assert not scm.GIT.IsWorkTreeDirty(patch_dir)
+
+ # Update the branch file in the master
+ patch_git('checkout', 'master')
+
+ def update_branch():
+ with open(branch_file, 'w') as f:
+ f.write(target_branch)
+ add_and_commit(branch_file, description_filename)
+ def push():
+ patch_git('push', 'origin', 'master', target_branch)
+ def push_fixup():
+ patch_git('fetch', 'origin')
+ patch_git('reset', '--hard', 'origin/master')
+ update_branch()
+ def push_failed():
+ raise NoTryServerAccess(str(e))
+
+ update_branch()
+ logging.info('Pushing patch')
+ _retry(push, push_fixup, push_failed)
+ except subprocess2.CalledProcessError, e:
+ # Restore state.
+ patch_git('checkout', 'master')
+ patch_git('reset', '--hard', 'origin/master')
+ raise
+
+
+def _retry(action, fixup, fail, attempts=3):
+ """Calls action until it passes without exceptions"""
+ for attempt in xrange(attempts):
+ try:
+ action()
+ except subprocess2.CalledProcessError:
+ is_last = attempt == attempts - 1
+ if is_last:
+ fail()
+ else:
+ fixup()
def PrintSuccess(bot_spec, options):
@@ -651,7 +802,9 @@ def gen_parser(prog):
" and exit. Do not send patch. Like --dry_run"
" but less verbose.")
group.add_option("-r", "--revision",
- help="Revision to use for the try job; default: the "
+ help="Revision to use for the try job. If 'auto' is "
+ "specified, it is resolved to the revision a patch is "
+ "generated against (Git only). Default: the "
"revision will be determined by the try server; see "
"its waterfall for more info")
group.add_option("-c", "--clobber", action="store_true",
@@ -737,6 +890,18 @@ def gen_parser(prog):
help="SVN url to use to write the changes in; --use_svn is "
"implied when using --svn_repo")
parser.add_option_group(group)
+
+ group = optparse.OptionGroup(parser, "Access the try server with Git")
+ group.add_option("--use_git",
ghost stip (do not use) 2014/03/28 23:24:08 it doesn't look like this option is used, please d
nodir 2014/03/31 20:25:14 It is used the same way as use_svn. If set, the op
+ action="store_const",
+ const=_SendChangeGit,
+ dest="send_patch",
+ help="Use GIT to talk to the try server")
+ group.add_option("-G", "--git_repo",
+ metavar="GIT_URL",
+ help="GIT url to use to write the changes in; --use_git is "
+ "implied when using --git_repo")
+ parser.add_option_group(group)
return parser
@@ -805,7 +970,6 @@ def TryChange(argv,
try:
changed_files = None
# Always include os.getcwd() in the checkout settings.
- checkouts = []
path = os.getcwd()
file_list = []
@@ -819,12 +983,29 @@ def TryChange(argv,
# Clear file list so that the correct list will be retrieved from the
# upstream branch.
file_list = []
- checkouts.append(GuessVCS(options, path, file_list))
- checkouts[0].AutomagicalSettings()
+
+ current_vcs = GuessVCS(options, path, file_list)
+ current_vcs.AutomagicalSettings()
+ options = current_vcs.options
+ vcs_is_git = type(current_vcs) is GIT
+
+ # So far, git_repo doesn't work with SVN
+ if options.git_repo and not vcs_is_git:
+ parser.error('--git_repo option is supported only for GIT repositories')
+
+ # If revision==auto, resolve it
+ if options.revision and options.revision.lower() == 'auto':
+ if not vcs_is_git:
+ parser.error('--revision=auto is supported only for GIT repositories')
+ options.revision = scm.GIT.Capture(
+ ['rev-parse', current_vcs.diff_against],
+ cwd=path)
+
+ checkouts = [current_vcs]
for item in options.sub_rep:
# Pass file_list=None because we don't know the sub repo's file list.
checkout = GuessVCS(options,
- os.path.join(checkouts[0].checkout_root, item),
+ os.path.join(current_vcs.checkout_root, item),
None)
if checkout.checkout_root in [c.checkout_root for c in checkouts]:
parser.error('Specified the root %s two times.' %
@@ -833,9 +1014,10 @@ def TryChange(argv,
can_http = options.port and options.host
can_svn = options.svn_repo
+ can_git = options.git_repo
# If there was no transport selected yet, now we must have enough data to
# select one.
- if not options.send_patch and not (can_http or can_svn):
+ if not options.send_patch and not (can_http or can_svn or can_git):
parser.error('Please specify an access method.')
# Convert options.diff into the content of the diff.
@@ -913,23 +1095,30 @@ def TryChange(argv,
print ' %s' % (bot[0])
return 0
- # Send the patch.
+ # Determine sending protocol
if options.send_patch:
# If forced.
- options.send_patch(bot_spec, options)
- PrintSuccess(bot_spec, options)
- return 0
- try:
- if can_http:
- _SendChangeHTTP(bot_spec, options)
+ senders = [options.send_patch]
+ else:
+ # Try sending patch using avaialble protocols
+ all_senders = [
+ (_SendChangeHTTP, can_http),
+ (_SendChangeSVN, can_svn),
+ (_SendChangeGit, can_git)
+ ]
+ senders = [sender for sender, can in all_senders if can]
+
+ # Send the patch.
+ for sender in senders:
+ try:
+ sender(bot_spec, options)
PrintSuccess(bot_spec, options)
return 0
- except NoTryServerAccess:
- if not can_svn:
- raise
- _SendChangeSVN(bot_spec, options)
- PrintSuccess(bot_spec, options)
- return 0
+ except NoTryServerAccess:
+ is_last = sender == senders[-1]
+ if is_last:
+ raise
+ assert False, "Unreachable code"
except (InvalidScript, NoTryServerAccess), e:
if swallow_exception:
return 1
« no previous file with comments | « tests/trychange_unittest.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698