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']) |