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

Side by Side Diff: git_cl.py

Issue 13741015: Switch ChangeDescription usage to be stricter. (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/depot_tools.git@master
Patch Set: append_line -> append_footer Created 7 years, 8 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 unified diff | Download patch
« no previous file with comments | « no previous file | tests/git_cl_test.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 # Copyright (C) 2008 Evan Martin <martine@danga.com> 6 # Copyright (C) 2008 Evan Martin <martine@danga.com>
7 7
8 """A git-command for integrating reviews on Rietveld.""" 8 """A git-command for integrating reviews on Rietveld."""
9 9
10 import json 10 import json
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after
47 47
48 48
49 def DieWithError(message): 49 def DieWithError(message):
50 print >> sys.stderr, message 50 print >> sys.stderr, message
51 sys.exit(1) 51 sys.exit(1)
52 52
53 53
54 def RunCommand(args, error_ok=False, error_message=None, **kwargs): 54 def RunCommand(args, error_ok=False, error_message=None, **kwargs):
55 try: 55 try:
56 return subprocess2.check_output(args, shell=False, **kwargs) 56 return subprocess2.check_output(args, shell=False, **kwargs)
57 except subprocess2.CalledProcessError, e: 57 except subprocess2.CalledProcessError as e:
58 logging.debug('Failed running %s', args)
58 if not error_ok: 59 if not error_ok:
59 DieWithError( 60 DieWithError(
60 'Command "%s" failed.\n%s' % ( 61 'Command "%s" failed.\n%s' % (
61 ' '.join(args), error_message or e.stdout or '')) 62 ' '.join(args), error_message or e.stdout or ''))
62 return e.stdout 63 return e.stdout
63 64
64 65
65 def RunGit(args, **kwargs): 66 def RunGit(args, **kwargs):
66 """Returns stdout.""" 67 """Returns stdout."""
67 return RunCommand(['git'] + args, **kwargs) 68 return RunCommand(['git'] + args, **kwargs)
(...skipping 722 matching lines...) Expand 10 before | Expand all | Expand 10 after
790 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL', 791 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
791 'tree-status-url', False) 792 'tree-status-url', False)
792 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True) 793 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
793 794
794 # TODO: configure a default branch to diff against, rather than this 795 # TODO: configure a default branch to diff against, rather than this
795 # svn-based hackery. 796 # svn-based hackery.
796 797
797 798
798 class ChangeDescription(object): 799 class ChangeDescription(object):
799 """Contains a parsed form of the change description.""" 800 """Contains a parsed form of the change description."""
800 def __init__(self, log_desc, reviewers): 801 R_LINE = r'^\s*(TBR|R)\s*=\s*(.+)\s*$'
801 self.log_desc = log_desc
802 self.reviewers = reviewers
803 self.description = self.log_desc
804 802
805 def Prompt(self): 803 def __init__(self, description):
806 content = """# Enter a description of the change. 804 self._description = (description or '').strip()
807 # This will displayed on the codereview site. 805
808 # The first line will also be used as the subject of the review. 806 @property
809 """ 807 def description(self):
810 content += self.description 808 return self._description
811 if ('\nR=' not in self.description and 809
812 '\nTBR=' not in self.description and 810 def update_reviewers(self, reviewers):
813 self.reviewers): 811 """Rewrites the R=/TBR= line(s) as a single line."""
814 content += '\nR=' + self.reviewers 812 assert isinstance(reviewers, list), reviewers
815 if '\nBUG=' not in self.description: 813 if not reviewers:
816 content += '\nBUG=' 814 return
817 content = content.rstrip('\n') + '\n' 815 regexp = re.compile(self.R_LINE, re.MULTILINE)
818 content = gclient_utils.RunEditor(content, True) 816 matches = list(regexp.finditer(self._description))
817 is_tbr = any(m.group(1) == 'TBR' for m in matches)
818 if len(matches) > 1:
819 # Erase all except the first one.
820 for i in xrange(len(matches) - 1, 0, -1):
821 self._description = (
822 self._description[:matches[i].start()] +
823 self._description[matches[i].end()+1:])
824
825 if is_tbr:
826 new_r_line = 'TBR=' + ', '.join(reviewers)
827 else:
828 new_r_line = 'R=' + ', '.join(reviewers)
829
830 if matches:
831 self._description = (
832 self._description[:matches[0].start()] + new_r_line +
833 self._description[matches[0].end()+1:])
834 else:
835 self.append_footer(new_r_line)
836
837 def prompt(self):
838 """Asks the user to update the description."""
839 self._description = (
840 '# Enter a description of the change.\n'
841 '# This will displayed on the codereview site.\n'
842 '# The first line will also be used as the subject of the review.\n'
843 ) + self._description
844
845 if '\nBUG=' not in self._description:
846 self.append_footer('BUG=')
847 content = gclient_utils.RunEditor(self._description, True)
819 if not content: 848 if not content:
820 DieWithError('Running editor failed') 849 DieWithError('Running editor failed')
850
851 # Strip off comments.
821 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip() 852 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
822 if not content.strip(): 853 if not content:
823 DieWithError('No CL description, aborting') 854 DieWithError('No CL description, aborting')
824 self.description = content 855 self._description = content
825 856
826 def ParseDescription(self): 857 def append_footer(self, line):
827 """Updates the list of reviewers and subject from the description.""" 858 # Adds a LF if the last line did not have 'FOO=BAR' or if the new one isn't.
828 self.description = self.description.strip('\n') + '\n' 859 if self._description:
829 # Retrieves all reviewer lines 860 if '\n' not in self._description:
830 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE) 861 self._description += '\n'
831 reviewers = ', '.join( 862 else:
832 i.group(2).strip() for i in regexp.finditer(self.description)) 863 last_line = self._description.rsplit('\n', 1)[1]
833 if reviewers: 864 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
834 self.reviewers = reviewers 865 not presubmit_support.Change.TAG_LINE_RE.match(line)):
866 self._description += '\n'
867 self._description += '\n' + line
835 868
836 def IsEmpty(self): 869 def get_reviewers(self):
837 return not self.description 870 """Retrieves the list of reviewers."""
871 regexp = re.compile(self.R_LINE, re.MULTILINE)
872 reviewers = [i.group(2) for i in regexp.finditer(self._description)]
873 return cleanup_list(reviewers)
838 874
839 875
840 def FindCodereviewSettingsFile(filename='codereview.settings'): 876 def FindCodereviewSettingsFile(filename='codereview.settings'):
841 """Finds the given file starting in the cwd and going up. 877 """Finds the given file starting in the cwd and going up.
842 878
843 Only looks up to the top of the repository unless an 879 Only looks up to the top of the repository unless an
844 'inherit-review-settings-ok' file exists in the root of the repository. 880 'inherit-review-settings-ok' file exists in the root of the repository.
845 """ 881 """
846 inherit_ok_file = 'inherit-review-settings-ok' 882 inherit_ok_file = 'inherit-review-settings-ok'
847 cwd = os.getcwd() 883 cwd = os.getcwd()
(...skipping 247 matching lines...) Expand 10 before | Expand all | Expand 10 after
1095 1131
1096 def GerritUpload(options, args, cl): 1132 def GerritUpload(options, args, cl):
1097 """upload the current branch to gerrit.""" 1133 """upload the current branch to gerrit."""
1098 # We assume the remote called "origin" is the one we want. 1134 # We assume the remote called "origin" is the one we want.
1099 # It is probably not worthwhile to support different workflows. 1135 # It is probably not worthwhile to support different workflows.
1100 remote = 'origin' 1136 remote = 'origin'
1101 branch = 'master' 1137 branch = 'master'
1102 if options.target_branch: 1138 if options.target_branch:
1103 branch = options.target_branch 1139 branch = options.target_branch
1104 1140
1105 log_desc = options.message or CreateDescriptionFromLog(args) 1141 change_desc = ChangeDescription(
1106 if CHANGE_ID not in log_desc: 1142 options.message or CreateDescriptionFromLog(args))
1143 if not change_desc.description:
1144 print "Description is empty; aborting."
1145 return 1
1146 if CHANGE_ID not in change_desc.description:
1107 AddChangeIdToCommitMessage(options, args) 1147 AddChangeIdToCommitMessage(options, args)
1108 if options.reviewers: 1148 if options.reviewers:
1109 log_desc += '\nR=' + ', '.join(options.reviewers) 1149 change_desc.update_reviewers(options.reviewers)
1110 change_desc = ChangeDescription(log_desc, ', '.join(options.reviewers))
1111 change_desc.ParseDescription()
1112 if change_desc.IsEmpty():
1113 print "Description is empty; aborting."
1114 return 1
1115 1150
1116 receive_options = [] 1151 receive_options = []
1117 cc = cl.GetCCList().split(',') 1152 cc = cl.GetCCList().split(',')
1118 if options.cc: 1153 if options.cc:
1119 cc.extend(options.cc) 1154 cc.extend(options.cc)
1120 cc = filter(None, cc) 1155 cc = filter(None, cc)
1121 if cc: 1156 if cc:
1122 receive_options += ['--cc=' + email for email in cc] 1157 receive_options += ['--cc=' + email for email in cc]
1123 if change_desc.reviewers: 1158 if change_desc.get_reviewers():
1124 reviewers = filter( 1159 receive_options.extend(
1125 None, (r.strip() for r in change_desc.reviewers.split(','))) 1160 '--reviewer=' + email for email in change_desc.get_reviewers())
1126 if reviewers:
1127 receive_options += ['--reviewer=' + email for email in reviewers]
1128 1161
1129 git_command = ['push'] 1162 git_command = ['push']
1130 if receive_options: 1163 if receive_options:
1131 git_command.append('--receive-pack=git receive-pack %s' % 1164 git_command.append('--receive-pack=git receive-pack %s' %
1132 ' '.join(receive_options)) 1165 ' '.join(receive_options))
1133 git_command += [remote, 'HEAD:refs/for/' + branch] 1166 git_command += [remote, 'HEAD:refs/for/' + branch]
1134 RunGit(git_command) 1167 RunGit(git_command)
1135 # TODO(ukai): parse Change-Id: and set issue number? 1168 # TODO(ukai): parse Change-Id: and set issue number?
1136 return 0 1169 return 0
1137 1170
(...skipping 15 matching lines...) Expand all
1153 # for upload.py. Soon this will be changed to set the --message option. 1186 # for upload.py. Soon this will be changed to set the --message option.
1154 # Will wait until people are used to typing -t instead of -m. 1187 # Will wait until people are used to typing -t instead of -m.
1155 upload_args.extend(['--title', options.message]) 1188 upload_args.extend(['--title', options.message])
1156 upload_args.extend(['--issue', str(cl.GetIssue())]) 1189 upload_args.extend(['--issue', str(cl.GetIssue())])
1157 print ("This branch is associated with issue %s. " 1190 print ("This branch is associated with issue %s. "
1158 "Adding patch to that issue." % cl.GetIssue()) 1191 "Adding patch to that issue." % cl.GetIssue())
1159 else: 1192 else:
1160 if options.title: 1193 if options.title:
1161 upload_args.extend(['--title', options.title]) 1194 upload_args.extend(['--title', options.title])
1162 message = options.title or options.message or CreateDescriptionFromLog(args) 1195 message = options.title or options.message or CreateDescriptionFromLog(args)
1163 change_desc = ChangeDescription(message, ','.join(options.reviewers)) 1196 change_desc = ChangeDescription(message)
1197 if options.reviewers:
1198 change_desc.update_reviewers(options.reviewers)
1164 if not options.force: 1199 if not options.force:
1165 change_desc.Prompt() 1200 change_desc.prompt()
1166 change_desc.ParseDescription()
1167 1201
1168 if change_desc.IsEmpty(): 1202 if not change_desc.description:
1169 print "Description is empty; aborting." 1203 print "Description is empty; aborting."
1170 return 1 1204 return 1
1171 1205
1172 upload_args.extend(['--message', change_desc.description]) 1206 upload_args.extend(['--message', change_desc.description])
1173 if change_desc.reviewers: 1207 if change_desc.get_reviewers():
1174 upload_args.extend( 1208 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
1175 [
1176 '--reviewers',
1177 ','.join(r.strip() for r in change_desc.reviewers.split(',')),
1178 ])
1179 if options.send_mail: 1209 if options.send_mail:
1180 if not change_desc.reviewers: 1210 if not change_desc.get_reviewers():
1181 DieWithError("Must specify reviewers to send email.") 1211 DieWithError("Must specify reviewers to send email.")
1182 upload_args.append('--send_mail') 1212 upload_args.append('--send_mail')
1183 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc)))) 1213 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
1184 if cc: 1214 if cc:
1185 upload_args.extend(['--cc', cc]) 1215 upload_args.extend(['--cc', cc])
1186 1216
1187 upload_args.extend(['--git_similarity', str(options.similarity)]) 1217 upload_args.extend(['--git_similarity', str(options.similarity)])
1188 if not options.find_copies: 1218 if not options.find_copies:
1189 upload_args.extend(['--git_no_find_copies']) 1219 upload_args.extend(['--git_no_find_copies'])
1190 1220
(...skipping 242 matching lines...) Expand 10 before | Expand all | Expand 10 after
1433 elif 'unknown' == status: 1463 elif 'unknown' == status:
1434 print('Unable to determine tree status. Please verify manually and ' 1464 print('Unable to determine tree status. Please verify manually and '
1435 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.') 1465 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
1436 else: 1466 else:
1437 breakpad.SendStack( 1467 breakpad.SendStack(
1438 'GitClHooksBypassedCommit', 1468 'GitClHooksBypassedCommit',
1439 'Issue %s/%s bypassed hook when committing' % 1469 'Issue %s/%s bypassed hook when committing' %
1440 (cl.GetRietveldServer(), cl.GetIssue()), 1470 (cl.GetRietveldServer(), cl.GetIssue()),
1441 verbose=False) 1471 verbose=False)
1442 1472
1443 description = options.message 1473 change_desc = ChangeDescription(options.message)
1444 if not description and cl.GetIssue(): 1474 if not change_desc.description and cl.GetIssue():
1445 description = cl.GetDescription() 1475 change_desc = ChangeDescription(cl.GetDescription())
1446 1476
1447 if not description: 1477 if not change_desc.description:
1448 if not cl.GetIssue() and options.bypass_hooks: 1478 if not cl.GetIssue() and options.bypass_hooks:
1449 description = CreateDescriptionFromLog([base_branch]) 1479 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
1450 else: 1480 else:
1451 print 'No description set.' 1481 print 'No description set.'
1452 print 'Visit %s/edit to set it.' % (cl.GetIssueURL()) 1482 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1453 return 1 1483 return 1
1454 1484
1485 # Keep a separate copy for the commit message, because the commit message
1486 # contains the link to the Rietveld issue, while the Rietveld message contains
1487 # the commit viewvc url.
1488 commit_desc = ChangeDescription(change_desc.description)
1455 if cl.GetIssue(): 1489 if cl.GetIssue():
1456 description += "\n\nReview URL: %s" % cl.GetIssueURL() 1490 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
1491 if options.contributor:
1492 commit_desc.append_footer('Patch from %s.' % options.contributor)
1457 1493
1458 if options.contributor: 1494 print 'Description:', repr(commit_desc.description)
1459 description += "\nPatch from %s." % options.contributor
1460 print 'Description:', repr(description)
1461 1495
1462 branches = [base_branch, cl.GetBranchRef()] 1496 branches = [base_branch, cl.GetBranchRef()]
1463 if not options.force: 1497 if not options.force:
1464 print_stats(options.similarity, options.find_copies, branches) 1498 print_stats(options.similarity, options.find_copies, branches)
1465 ask_for_data('About to commit; enter to confirm.') 1499 ask_for_data('About to commit; enter to confirm.')
1466 1500
1467 # We want to squash all this branch's commits into one commit with the proper 1501 # We want to squash all this branch's commits into one commit with the proper
1468 # description. We do this by doing a "reset --soft" to the base branch (which 1502 # description. We do this by doing a "reset --soft" to the base branch (which
1469 # keeps the working copy the same), then dcommitting that. If origin/master 1503 # keeps the working copy the same), then dcommitting that. If origin/master
1470 # has a submodule merge commit, we'll also need to cherry-pick the squashed 1504 # has a submodule merge commit, we'll also need to cherry-pick the squashed
(...skipping 15 matching lines...) Expand all
1486 os.chdir(rel_base_path) 1520 os.chdir(rel_base_path)
1487 1521
1488 # Stuff our change into the merge branch. 1522 # Stuff our change into the merge branch.
1489 # We wrap in a try...finally block so if anything goes wrong, 1523 # We wrap in a try...finally block so if anything goes wrong,
1490 # we clean up the branches. 1524 # we clean up the branches.
1491 retcode = -1 1525 retcode = -1
1492 try: 1526 try:
1493 RunGit(['checkout', '-q', '-b', MERGE_BRANCH]) 1527 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1494 RunGit(['reset', '--soft', base_branch]) 1528 RunGit(['reset', '--soft', base_branch])
1495 if options.contributor: 1529 if options.contributor:
1496 RunGit(['commit', '--author', options.contributor, '-m', description]) 1530 RunGit(
1531 [
1532 'commit', '--author', options.contributor,
1533 '-m', commit_desc.description,
1534 ])
1497 else: 1535 else:
1498 RunGit(['commit', '-m', description]) 1536 RunGit(['commit', '-m', commit_desc.description])
1499 if base_has_submodules: 1537 if base_has_submodules:
1500 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip() 1538 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1501 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head]) 1539 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1502 RunGit(['checkout', CHERRY_PICK_BRANCH]) 1540 RunGit(['checkout', CHERRY_PICK_BRANCH])
1503 RunGit(['cherry-pick', cherry_pick_commit]) 1541 RunGit(['cherry-pick', cherry_pick_commit])
1504 if cmd == 'push': 1542 if cmd == 'push':
1505 # push the merge branch. 1543 # push the merge branch.
1506 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch()) 1544 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
1507 retcode, output = RunGitWithCode( 1545 retcode, output = RunGitWithCode(
1508 ['push', '--porcelain', remote, 'HEAD:%s' % branch]) 1546 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
(...skipping 18 matching lines...) Expand all
1527 for l in output.splitlines(False)) 1565 for l in output.splitlines(False))
1528 match = filter(None, match) 1566 match = filter(None, match)
1529 if len(match) != 1: 1567 if len(match) != 1:
1530 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" % 1568 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1531 output) 1569 output)
1532 revision = match[0].group(2) 1570 revision = match[0].group(2)
1533 else: 1571 else:
1534 return 1 1572 return 1
1535 viewvc_url = settings.GetViewVCUrl() 1573 viewvc_url = settings.GetViewVCUrl()
1536 if viewvc_url and revision: 1574 if viewvc_url and revision:
1537 cl.description += ('\n\nCommitted: ' + viewvc_url + revision) 1575 change_desc.append_footer('Committed: ' + viewvc_url + revision)
1538 elif revision: 1576 elif revision:
1539 cl.description += ('\n\nCommitted: ' + revision) 1577 change_desc.append_footer('Committed: ' + revision)
1540 print ('Closing issue ' 1578 print ('Closing issue '
1541 '(you may be prompted for your codereview password)...') 1579 '(you may be prompted for your codereview password)...')
1542 cl.UpdateDescription(cl.description) 1580 cl.UpdateDescription(change_desc.description)
1543 cl.CloseIssue() 1581 cl.CloseIssue()
1544 props = cl.RpcServer().get_issue_properties(cl.GetIssue(), False) 1582 props = cl.RpcServer().get_issue_properties(cl.GetIssue(), False)
1545 patch_num = len(props['patchsets']) 1583 patch_num = len(props['patchsets'])
1546 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision) 1584 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
1547 comment += ' (presubmit successful).' if not options.bypass_hooks else '.' 1585 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1548 cl.RpcServer().add_comment(cl.GetIssue(), comment) 1586 cl.RpcServer().add_comment(cl.GetIssue(), comment)
1549 cl.SetIssue(0) 1587 cl.SetIssue(0)
1550 1588
1551 if retcode == 0: 1589 if retcode == 0:
1552 hook = POSTUPSTREAM_HOOK_PATTERN % cmd 1590 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
(...skipping 376 matching lines...) Expand 10 before | Expand all | Expand 10 after
1929 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e))) 1967 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1930 1968
1931 # Not a known command. Default to help. 1969 # Not a known command. Default to help.
1932 GenUsage(parser, 'help') 1970 GenUsage(parser, 'help')
1933 return CMDhelp(parser, argv) 1971 return CMDhelp(parser, argv)
1934 1972
1935 1973
1936 if __name__ == '__main__': 1974 if __name__ == '__main__':
1937 fix_encoding.fix_encoding() 1975 fix_encoding.fix_encoding()
1938 sys.exit(main(sys.argv[1:])) 1976 sys.exit(main(sys.argv[1:]))
OLDNEW
« no previous file with comments | « no previous file | tests/git_cl_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698