| Index: git_cl.py
|
| diff --git a/git_cl.py b/git_cl.py
|
| index 35cfea3b7d3d9bb1d17df413008890c114b897b8..64cd9e86f091e00d0201b242187930fd393de70b 100755
|
| --- a/git_cl.py
|
| +++ b/git_cl.py
|
| @@ -54,7 +54,8 @@ def DieWithError(message):
|
| def RunCommand(args, error_ok=False, error_message=None, **kwargs):
|
| try:
|
| return subprocess2.check_output(args, shell=False, **kwargs)
|
| - except subprocess2.CalledProcessError, e:
|
| + except subprocess2.CalledProcessError as e:
|
| + logging.debug('Failed running %s', args)
|
| if not error_ok:
|
| DieWithError(
|
| 'Command "%s" failed.\n%s' % (
|
| @@ -797,44 +798,79 @@ def GetCodereviewSettingsInteractively():
|
|
|
| class ChangeDescription(object):
|
| """Contains a parsed form of the change description."""
|
| - def __init__(self, log_desc, reviewers):
|
| - self.log_desc = log_desc
|
| - self.reviewers = reviewers
|
| - self.description = self.log_desc
|
| -
|
| - def Prompt(self):
|
| - content = """# Enter a description of the change.
|
| -# This will displayed on the codereview site.
|
| -# The first line will also be used as the subject of the review.
|
| -"""
|
| - content += self.description
|
| - if ('\nR=' not in self.description and
|
| - '\nTBR=' not in self.description and
|
| - self.reviewers):
|
| - content += '\nR=' + self.reviewers
|
| - if '\nBUG=' not in self.description:
|
| - content += '\nBUG='
|
| - content = content.rstrip('\n') + '\n'
|
| - content = gclient_utils.RunEditor(content, True)
|
| + R_LINE = r'^\s*(TBR|R)\s*=\s*(.+)\s*$'
|
| +
|
| + def __init__(self, description):
|
| + self._description = (description or '').strip()
|
| +
|
| + @property
|
| + def description(self):
|
| + return self._description
|
| +
|
| + def update_reviewers(self, reviewers):
|
| + """Rewrites the R=/TBR= line(s) as a single line."""
|
| + assert isinstance(reviewers, list), reviewers
|
| + if not reviewers:
|
| + return
|
| + regexp = re.compile(self.R_LINE, re.MULTILINE)
|
| + matches = list(regexp.finditer(self._description))
|
| + is_tbr = any(m.group(1) == 'TBR' for m in matches)
|
| + if len(matches) > 1:
|
| + # Erase all except the first one.
|
| + for i in xrange(len(matches) - 1, 0, -1):
|
| + self._description = (
|
| + self._description[:matches[i].start()] +
|
| + self._description[matches[i].end()+1:])
|
| +
|
| + if is_tbr:
|
| + new_r_line = 'TBR=' + ', '.join(reviewers)
|
| + else:
|
| + new_r_line = 'R=' + ', '.join(reviewers)
|
| +
|
| + if matches:
|
| + self._description = (
|
| + self._description[:matches[0].start()] + new_r_line +
|
| + self._description[matches[0].end()+1:])
|
| + else:
|
| + self.append_footer(new_r_line)
|
| +
|
| + def prompt(self):
|
| + """Asks the user to update the description."""
|
| + self._description = (
|
| + '# Enter a description of the change.\n'
|
| + '# This will displayed on the codereview site.\n'
|
| + '# The first line will also be used as the subject of the review.\n'
|
| + ) + self._description
|
| +
|
| + if '\nBUG=' not in self._description:
|
| + self.append_footer('BUG=')
|
| + content = gclient_utils.RunEditor(self._description, True)
|
| if not content:
|
| DieWithError('Running editor failed')
|
| +
|
| + # Strip off comments.
|
| content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
|
| - if not content.strip():
|
| + if not content:
|
| DieWithError('No CL description, aborting')
|
| - self.description = content
|
| + self._description = content
|
|
|
| - def ParseDescription(self):
|
| - """Updates the list of reviewers and subject from the description."""
|
| - self.description = self.description.strip('\n') + '\n'
|
| - # Retrieves all reviewer lines
|
| - regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
|
| - reviewers = ', '.join(
|
| - i.group(2).strip() for i in regexp.finditer(self.description))
|
| - if reviewers:
|
| - self.reviewers = reviewers
|
| + def append_footer(self, line):
|
| + # Adds a LF if the last line did not have 'FOO=BAR' or if the new one isn't.
|
| + if self._description:
|
| + if '\n' not in self._description:
|
| + self._description += '\n'
|
| + else:
|
| + last_line = self._description.rsplit('\n', 1)[1]
|
| + if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
|
| + not presubmit_support.Change.TAG_LINE_RE.match(line)):
|
| + self._description += '\n'
|
| + self._description += '\n' + line
|
|
|
| - def IsEmpty(self):
|
| - return not self.description
|
| + def get_reviewers(self):
|
| + """Retrieves the list of reviewers."""
|
| + regexp = re.compile(self.R_LINE, re.MULTILINE)
|
| + reviewers = [i.group(2) for i in regexp.finditer(self._description)]
|
| + return cleanup_list(reviewers)
|
|
|
|
|
| def FindCodereviewSettingsFile(filename='codereview.settings'):
|
| @@ -1102,16 +1138,15 @@ def GerritUpload(options, args, cl):
|
| if options.target_branch:
|
| branch = options.target_branch
|
|
|
| - log_desc = options.message or CreateDescriptionFromLog(args)
|
| - if CHANGE_ID not in log_desc:
|
| - AddChangeIdToCommitMessage(options, args)
|
| - if options.reviewers:
|
| - log_desc += '\nR=' + ', '.join(options.reviewers)
|
| - change_desc = ChangeDescription(log_desc, ', '.join(options.reviewers))
|
| - change_desc.ParseDescription()
|
| - if change_desc.IsEmpty():
|
| + change_desc = ChangeDescription(
|
| + options.message or CreateDescriptionFromLog(args))
|
| + if not change_desc.description:
|
| print "Description is empty; aborting."
|
| return 1
|
| + if CHANGE_ID not in change_desc.description:
|
| + AddChangeIdToCommitMessage(options, args)
|
| + if options.reviewers:
|
| + change_desc.update_reviewers(options.reviewers)
|
|
|
| receive_options = []
|
| cc = cl.GetCCList().split(',')
|
| @@ -1120,11 +1155,9 @@ def GerritUpload(options, args, cl):
|
| cc = filter(None, cc)
|
| if cc:
|
| receive_options += ['--cc=' + email for email in cc]
|
| - if change_desc.reviewers:
|
| - reviewers = filter(
|
| - None, (r.strip() for r in change_desc.reviewers.split(',')))
|
| - if reviewers:
|
| - receive_options += ['--reviewer=' + email for email in reviewers]
|
| + if change_desc.get_reviewers():
|
| + receive_options.extend(
|
| + '--reviewer=' + email for email in change_desc.get_reviewers())
|
|
|
| git_command = ['push']
|
| if receive_options:
|
| @@ -1160,24 +1193,21 @@ def RietveldUpload(options, args, cl):
|
| if options.title:
|
| upload_args.extend(['--title', options.title])
|
| message = options.title or options.message or CreateDescriptionFromLog(args)
|
| - change_desc = ChangeDescription(message, ','.join(options.reviewers))
|
| + change_desc = ChangeDescription(message)
|
| + if options.reviewers:
|
| + change_desc.update_reviewers(options.reviewers)
|
| if not options.force:
|
| - change_desc.Prompt()
|
| - change_desc.ParseDescription()
|
| + change_desc.prompt()
|
|
|
| - if change_desc.IsEmpty():
|
| + if not change_desc.description:
|
| print "Description is empty; aborting."
|
| return 1
|
|
|
| upload_args.extend(['--message', change_desc.description])
|
| - if change_desc.reviewers:
|
| - upload_args.extend(
|
| - [
|
| - '--reviewers',
|
| - ','.join(r.strip() for r in change_desc.reviewers.split(',')),
|
| - ])
|
| + if change_desc.get_reviewers():
|
| + upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
|
| if options.send_mail:
|
| - if not change_desc.reviewers:
|
| + if not change_desc.get_reviewers():
|
| DieWithError("Must specify reviewers to send email.")
|
| upload_args.append('--send_mail')
|
| cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
|
| @@ -1440,24 +1470,28 @@ def SendUpstream(parser, args, cmd):
|
| (cl.GetRietveldServer(), cl.GetIssue()),
|
| verbose=False)
|
|
|
| - description = options.message
|
| - if not description and cl.GetIssue():
|
| - description = cl.GetDescription()
|
| + change_desc = ChangeDescription(options.message)
|
| + if not change_desc.description and cl.GetIssue():
|
| + change_desc = ChangeDescription(cl.GetDescription())
|
|
|
| - if not description:
|
| + if not change_desc.description:
|
| if not cl.GetIssue() and options.bypass_hooks:
|
| - description = CreateDescriptionFromLog([base_branch])
|
| + change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
|
| else:
|
| print 'No description set.'
|
| print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
|
| return 1
|
|
|
| + # Keep a separate copy for the commit message, because the commit message
|
| + # contains the link to the Rietveld issue, while the Rietveld message contains
|
| + # the commit viewvc url.
|
| + commit_desc = ChangeDescription(change_desc.description)
|
| if cl.GetIssue():
|
| - description += "\n\nReview URL: %s" % cl.GetIssueURL()
|
| -
|
| + commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
|
| if options.contributor:
|
| - description += "\nPatch from %s." % options.contributor
|
| - print 'Description:', repr(description)
|
| + commit_desc.append_footer('Patch from %s.' % options.contributor)
|
| +
|
| + print 'Description:', repr(commit_desc.description)
|
|
|
| branches = [base_branch, cl.GetBranchRef()]
|
| if not options.force:
|
| @@ -1493,9 +1527,13 @@ def SendUpstream(parser, args, cmd):
|
| RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
|
| RunGit(['reset', '--soft', base_branch])
|
| if options.contributor:
|
| - RunGit(['commit', '--author', options.contributor, '-m', description])
|
| + RunGit(
|
| + [
|
| + 'commit', '--author', options.contributor,
|
| + '-m', commit_desc.description,
|
| + ])
|
| else:
|
| - RunGit(['commit', '-m', description])
|
| + RunGit(['commit', '-m', commit_desc.description])
|
| if base_has_submodules:
|
| cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
|
| RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
|
| @@ -1534,12 +1572,12 @@ def SendUpstream(parser, args, cmd):
|
| return 1
|
| viewvc_url = settings.GetViewVCUrl()
|
| if viewvc_url and revision:
|
| - cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
|
| + change_desc.append_footer('Committed: ' + viewvc_url + revision)
|
| elif revision:
|
| - cl.description += ('\n\nCommitted: ' + revision)
|
| + change_desc.append_footer('Committed: ' + revision)
|
| print ('Closing issue '
|
| '(you may be prompted for your codereview password)...')
|
| - cl.UpdateDescription(cl.description)
|
| + cl.UpdateDescription(change_desc.description)
|
| cl.CloseIssue()
|
| props = cl.RpcServer().get_issue_properties(cl.GetIssue(), False)
|
| patch_num = len(props['patchsets'])
|
|
|