Commit b1391c6a authored by nodir@chromium.org's avatar nodir@chromium.org

Now trychange can store patches in a Git repo

A git patch repo is cloned to .git/git-try/patches-git, if was not
cloned before. Otherwise, changes are pulled. Then a patch is committed and
pushed.

--revision=auto (Git only) is resolved to the revision the diff is generated
against.

R=stip@chromium.org, agable@chromium.org, iannucci@chromium.org, maruel@chromium.org
BUG=325882

Review URL: https://codereview.chromium.org/184343003

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@260471 0039d316-1c4b-4281-b951-d872f2087c98
parent 2d3c28d5
...@@ -140,6 +140,10 @@ class GIT(object): ...@@ -140,6 +140,10 @@ class GIT(object):
results.append(('%s ' % m.group(1)[0], m.group(2))) results.append(('%s ' % m.group(1)[0], m.group(2)))
return results return results
@staticmethod
def IsWorkTreeDirty(cwd):
return GIT.Capture(['status', '-s'], cwd=cwd) != ''
@staticmethod @staticmethod
def GetEmail(cwd): def GetEmail(cwd):
"""Retrieves the user email address if known.""" """Retrieves the user email address if known."""
...@@ -389,6 +393,17 @@ class GIT(object): ...@@ -389,6 +393,17 @@ class GIT(object):
root = GIT.Capture(['rev-parse', '--show-cdup'], cwd=cwd) root = GIT.Capture(['rev-parse', '--show-cdup'], cwd=cwd)
return os.path.abspath(os.path.join(cwd, root)) return os.path.abspath(os.path.join(cwd, root))
@staticmethod
def GetGitDir(cwd):
return os.path.abspath(GIT.Capture(['rev-parse', '--git-dir'], cwd=cwd))
@staticmethod
def IsInsideWorkTree(cwd):
try:
return GIT.Capture(['rev-parse', '--is-inside-work-tree'], cwd=cwd)
except (OSError, subprocess2.CalledProcessError):
return False
@staticmethod @staticmethod
def GetGitSvnHeadRev(cwd): def GetGitSvnHeadRev(cwd):
"""Gets the most recently pulled git-svn revision.""" """Gets the most recently pulled git-svn revision."""
......
...@@ -85,13 +85,16 @@ class GitWrapperTestCase(BaseSCMTestCase): ...@@ -85,13 +85,16 @@ class GitWrapperTestCase(BaseSCMTestCase):
'GetCheckoutRoot', 'GetCheckoutRoot',
'GetDifferentFiles', 'GetDifferentFiles',
'GetEmail', 'GetEmail',
'GetGitDir',
'GetGitSvnHeadRev', 'GetGitSvnHeadRev',
'GetPatchName', 'GetPatchName',
'GetSha1ForSvnRev', 'GetSha1ForSvnRev',
'GetSVNBranch', 'GetSVNBranch',
'GetUpstreamBranch', 'GetUpstreamBranch',
'IsGitSvn', 'IsGitSvn',
'IsInsideWorkTree',
'IsValidRevision', 'IsValidRevision',
'IsWorkTreeDirty',
'MatchSvnGlob', 'MatchSvnGlob',
'ParseGitSvnSha1', 'ParseGitSvnSha1',
'ShortBranchName', 'ShortBranchName',
......
...@@ -46,10 +46,12 @@ class TryChangeUnittest(TryChangeTestsBase): ...@@ -46,10 +46,12 @@ class TryChangeUnittest(TryChangeTestsBase):
"""General trychange.py tests.""" """General trychange.py tests."""
def testMembersChanged(self): def testMembersChanged(self):
members = [ members = [
'DieWithError', 'EPILOG', 'Escape', 'GIT', 'GetMungedDiff', 'GuessVCS', 'DieWithError', 'EPILOG', 'Escape', 'GIT', 'GIT_PATCH_DIR_BASENAME',
'GetMungedDiff', 'GuessVCS', 'GIT_BRANCH_FILE',
'HELP_STRING', 'InvalidScript', 'NoTryServerAccess', 'OptionParser', 'HELP_STRING', 'InvalidScript', 'NoTryServerAccess', 'OptionParser',
'PrintSuccess', 'PrintSuccess',
'RunCommand', 'RunGit', 'SCM', 'SVN', 'TryChange', 'USAGE', 'breakpad', 'RunCommand', 'RunGit', 'SCM', 'SVN', 'TryChange', 'USAGE', 'contextlib',
'breakpad',
'datetime', 'errno', 'fix_encoding', 'gcl', 'gclient_utils', 'gen_parser', 'datetime', 'errno', 'fix_encoding', 'gcl', 'gclient_utils', 'gen_parser',
'getpass', 'itertools', 'json', 'logging', 'optparse', 'os', 'posixpath', 'getpass', 'itertools', 'json', 'logging', 'optparse', 'os', 'posixpath',
're', 'scm', 'shutil', 'subprocess2', 'sys', 'tempfile', 'urllib', 're', 'scm', 'shutil', 'subprocess2', 'sys', 'tempfile', 'urllib',
......
...@@ -4,10 +4,11 @@ ...@@ -4,10 +4,11 @@
# found in the LICENSE file. # found in the LICENSE file.
"""Client-side script to send a try job to the try server. It communicates to """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 the try server by either writting to a svn/git repository or by directly
to the server by HTTP. connecting to the server by HTTP.
""" """
import contextlib
import datetime import datetime
import errno import errno
import getpass import getpass
...@@ -70,6 +71,9 @@ Examples: ...@@ -70,6 +71,9 @@ Examples:
-f include/b.h -f include/b.h
""" """
GIT_PATCH_DIR_BASENAME = os.path.join('git-try', 'patches-git')
GIT_BRANCH_FILE = 'ref'
_GIT_PUSH_ATTEMPTS = 3
def DieWithError(message): def DieWithError(message):
print >> sys.stderr, message print >> sys.stderr, message
...@@ -164,7 +168,10 @@ class SCM(object): ...@@ -164,7 +168,10 @@ class SCM(object):
'port': self.GetCodeReviewSetting('TRYSERVER_HTTP_PORT'), 'port': self.GetCodeReviewSetting('TRYSERVER_HTTP_PORT'),
'host': self.GetCodeReviewSetting('TRYSERVER_HTTP_HOST'), 'host': self.GetCodeReviewSetting('TRYSERVER_HTTP_HOST'),
'svn_repo': self.GetCodeReviewSetting('TRYSERVER_SVN_URL'), 'svn_repo': self.GetCodeReviewSetting('TRYSERVER_SVN_URL'),
'git_repo': self.GetCodeReviewSetting('TRYSERVER_GIT_URL'),
'project': self.GetCodeReviewSetting('TRYSERVER_PROJECT'), 'project': self.GetCodeReviewSetting('TRYSERVER_PROJECT'),
# Primarily for revision=auto
'revision': self.GetCodeReviewSetting('TRYSERVER_REVISION'),
'root': self.GetCodeReviewSetting('TRYSERVER_ROOT'), 'root': self.GetCodeReviewSetting('TRYSERVER_ROOT'),
'patchlevel': self.GetCodeReviewSetting('TRYSERVER_PATCHLEVEL'), 'patchlevel': self.GetCodeReviewSetting('TRYSERVER_PATCHLEVEL'),
} }
...@@ -410,7 +417,9 @@ def _GenTSBotSpec(checkouts, change, changed_files, options): ...@@ -410,7 +417,9 @@ def _GenTSBotSpec(checkouts, change, changed_files, options):
def _ParseSendChangeOptions(bot_spec, options): def _ParseSendChangeOptions(bot_spec, options):
"""Parse common options passed to _SendChangeHTTP and _SendChangeSVN.""" """Parse common options passed to _SendChangeHTTP, _SendChangeSVN and
_SendChangeGit.
"""
values = [ values = [
('user', options.user), ('user', options.user),
('name', options.name), ('name', options.name),
...@@ -481,6 +490,44 @@ def _SendChangeHTTP(bot_spec, options): ...@@ -481,6 +490,44 @@ def _SendChangeHTTP(bot_spec, options):
raise NoTryServerAccess('%s is unaccessible. Got:\n%s' % (url, response)) 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): def _SendChangeSVN(bot_spec, options):
"""Send a change to the try server by committing a diff file on a subversion """Send a change to the try server by committing a diff file on a subversion
server.""" server."""
...@@ -497,45 +544,141 @@ def _SendChangeSVN(bot_spec, options): ...@@ -497,45 +544,141 @@ def _SendChangeSVN(bot_spec, options):
if options.dry_run: if options.dry_run:
return return
# Create a temporary directory, put a uniquely named file in it with the diff with _PrepareDescriptionAndPatchFiles(description, options) as (
# content and svn import that. patch_filename, description_filename):
temp_dir = tempfile.mkdtemp() if sys.platform == "cygwin":
temp_file = tempfile.NamedTemporaryFile() # Small chromium-specific issue here:
try: # git-try uses /usr/bin/python on cygwin but svn.bat will be used
try: # instead of /usr/bin/svn by default. That causes bad things(tm) since
# Description # Windows' svn.exe has no clue about cygwin paths. Hence force to use
temp_file.write(description) # the cygwin version in this particular context.
temp_file.flush() exe = "/usr/bin/svn"
else:
# Diff file exe = "svn"
current_time = str(datetime.datetime.now()).replace(':', '.') patch_dir = os.path.dirname(patch_filename)
file_name = (Escape(options.user) + '.' + Escape(options.name) + command = [exe, 'import', '-q', patch_dir, options.svn_repo, '--file',
'.%s.diff' % current_time) description_filename]
full_path = os.path.join(temp_dir, file_name) if scm.SVN.AssertVersion("1.5")[0]:
with open(full_path, 'wb') as f: command.append('--no-ignore')
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')
try:
subprocess2.check_call(command) subprocess2.check_call(command)
except subprocess2.CalledProcessError, e: except subprocess2.CalledProcessError, e:
raise NoTryServerAccess(str(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)
update_branch()
# Push master and target_branch to origin.
logging.info('Pushing patch')
for attempt in xrange(_GIT_PUSH_ATTEMPTS):
try:
patch_git('push', 'origin', 'master', target_branch)
except subprocess2.CalledProcessError as e:
is_last = attempt == _GIT_PUSH_ATTEMPTS - 1
if is_last:
raise NoTryServerAccess(str(e))
# Fetch, reset, update branch file again.
patch_git('fetch', 'origin')
patch_git('reset', '--hard', 'origin/master')
update_branch()
except subprocess2.CalledProcessError, e:
# Restore state.
patch_git('checkout', 'master')
patch_git('reset', '--hard', 'origin/master')
raise
def PrintSuccess(bot_spec, options): def PrintSuccess(bot_spec, options):
...@@ -651,7 +794,9 @@ def gen_parser(prog): ...@@ -651,7 +794,9 @@ def gen_parser(prog):
" and exit. Do not send patch. Like --dry_run" " and exit. Do not send patch. Like --dry_run"
" but less verbose.") " but less verbose.")
group.add_option("-r", "--revision", 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 " "revision will be determined by the try server; see "
"its waterfall for more info") "its waterfall for more info")
group.add_option("-c", "--clobber", action="store_true", group.add_option("-c", "--clobber", action="store_true",
...@@ -737,6 +882,18 @@ def gen_parser(prog): ...@@ -737,6 +882,18 @@ def gen_parser(prog):
help="SVN url to use to write the changes in; --use_svn is " help="SVN url to use to write the changes in; --use_svn is "
"implied when using --svn_repo") "implied when using --svn_repo")
parser.add_option_group(group) parser.add_option_group(group)
group = optparse.OptionGroup(parser, "Access the try server with Git")
group.add_option("--use_git",
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 return parser
...@@ -805,7 +962,6 @@ def TryChange(argv, ...@@ -805,7 +962,6 @@ def TryChange(argv,
try: try:
changed_files = None changed_files = None
# Always include os.getcwd() in the checkout settings. # Always include os.getcwd() in the checkout settings.
checkouts = []
path = os.getcwd() path = os.getcwd()
file_list = [] file_list = []
...@@ -819,12 +975,29 @@ def TryChange(argv, ...@@ -819,12 +975,29 @@ def TryChange(argv,
# Clear file list so that the correct list will be retrieved from the # Clear file list so that the correct list will be retrieved from the
# upstream branch. # upstream branch.
file_list = [] 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: for item in options.sub_rep:
# Pass file_list=None because we don't know the sub repo's file list. # Pass file_list=None because we don't know the sub repo's file list.
checkout = GuessVCS(options, checkout = GuessVCS(options,
os.path.join(checkouts[0].checkout_root, item), os.path.join(current_vcs.checkout_root, item),
None) None)
if checkout.checkout_root in [c.checkout_root for c in checkouts]: if checkout.checkout_root in [c.checkout_root for c in checkouts]:
parser.error('Specified the root %s two times.' % parser.error('Specified the root %s two times.' %
...@@ -833,9 +1006,10 @@ def TryChange(argv, ...@@ -833,9 +1006,10 @@ def TryChange(argv,
can_http = options.port and options.host can_http = options.port and options.host
can_svn = options.svn_repo can_svn = options.svn_repo
can_git = options.git_repo
# If there was no transport selected yet, now we must have enough data to # If there was no transport selected yet, now we must have enough data to
# select one. # 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.') parser.error('Please specify an access method.')
# Convert options.diff into the content of the diff. # Convert options.diff into the content of the diff.
...@@ -913,23 +1087,30 @@ def TryChange(argv, ...@@ -913,23 +1087,30 @@ def TryChange(argv,
print ' %s' % (bot[0]) print ' %s' % (bot[0])
return 0 return 0
# Send the patch. # Determine sending protocol
if options.send_patch: if options.send_patch:
# If forced. # If forced.
options.send_patch(bot_spec, options) senders = [options.send_patch]
PrintSuccess(bot_spec, options) else:
return 0 # Try sending patch using avaialble protocols
try: all_senders = [
if can_http: (_SendChangeHTTP, can_http),
_SendChangeHTTP(bot_spec, options) (_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) PrintSuccess(bot_spec, options)
return 0 return 0
except NoTryServerAccess: except NoTryServerAccess:
if not can_svn: is_last = sender == senders[-1]
raise if is_last:
_SendChangeSVN(bot_spec, options) raise
PrintSuccess(bot_spec, options) assert False, "Unreachable code"
return 0
except (InvalidScript, NoTryServerAccess), e: except (InvalidScript, NoTryServerAccess), e:
if swallow_exception: if swallow_exception:
return 1 return 1
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment