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

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: 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):
iannucci 2013/04/08 22:49:40 Can we make reviewers a list of reviewers instead
Dirk Pranke 2013/04/09 00:07:52 I don't think we can easily do this without pushin
M-A Ruel 2013/04/11 00:01:38 So reviewers is now a list all the time.
813 self.reviewers): 811 """Rewrites the R=/TBR= line(s) as a single line."""
814 content += '\nR=' + self.reviewers 812 if not reviewers:
813 return
814 regexp = re.compile(self.R_LINE, re.MULTILINE)
815 matches = list(regexp.finditer(self.description))
816 is_tbr = any(m.group(1) == 'TBR' for m in matches)
817 if len(matches) > 1:
818 # Erase all except the first one.
819 for i in xrange(len(matches) - 1, 0, -1):
820 self._description = (
821 self.description[:matches[i].start()] +
822 self.description[matches[i].end()+1:])
Dirk Pranke 2013/04/09 00:07:52 switching back and forth between self._description
M-A Ruel 2013/04/11 00:01:38 Done.
823
824 if is_tbr:
825 new_r_line = 'TBR=' + reviewers
826 else:
827 new_r_line = 'R=' + reviewers
828
829 if matches:
830 self._description = (
831 self.description[:matches[0].start()] + new_r_line +
832 self.description[matches[0].end()+1:])
833 else:
834 self.append_line(new_r_line)
835
836 def prompt(self):
837 """Asks the user to update the description."""
838 self._description = (
839 '# Enter a description of the change.\n'
840 '# This will displayed on the codereview site.\n'
841 '# The first line will also be used as the subject of the review.\n'
842 ) + self.description
843
815 if '\nBUG=' not in self.description: 844 if '\nBUG=' not in self.description:
816 content += '\nBUG=' 845 self.append_line('BUG=')
817 content = content.rstrip('\n') + '\n' 846 content = gclient_utils.RunEditor(self.description, True)
818 content = gclient_utils.RunEditor(content, True)
819 if not content: 847 if not content:
820 DieWithError('Running editor failed') 848 DieWithError('Running editor failed')
849
850 # Strip off comments.
821 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip() 851 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
822 if not content.strip(): 852 if not content:
823 DieWithError('No CL description, aborting') 853 DieWithError('No CL description, aborting')
824 self.description = content 854 self._description = content
825 855
826 def ParseDescription(self): 856 def append_line(self, line):
Dirk Pranke 2013/04/09 00:07:52 I would probably call this something like add_keyw
827 """Updates the list of reviewers and subject from the description.""" 857 # 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' 858 if self.description:
829 # Retrieves all reviewer lines 859 if '\n' not in self.description:
830 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE) 860 self._description += '\n'
831 reviewers = ','.join( 861 else:
832 i.group(2).strip() for i in regexp.finditer(self.description)) 862 last_line = self.description.rsplit('\n', 1)[1]
833 if reviewers: 863 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
834 self.reviewers = reviewers 864 not presubmit_support.Change.TAG_LINE_RE.match(line)):
865 self._description += '\n'
866 self._description += '\n' + line
835 867
836 def IsEmpty(self): 868 def get_reviewers(self):
837 return not self.description 869 """Retrieves the list of reviewers."""
870 regexp = re.compile(self.R_LINE, re.MULTILINE)
871 reviewers = sum(
872 (i.group(2).split(',') for i in regexp.finditer(self.description)),
873 [])
874 reviewers = (i.strip() for i in reviewers)
875 return sorted(i for i in reviewers if i)
838 876
839 877
840 def FindCodereviewSettingsFile(filename='codereview.settings'): 878 def FindCodereviewSettingsFile(filename='codereview.settings'):
841 """Finds the given file starting in the cwd and going up. 879 """Finds the given file starting in the cwd and going up.
842 880
843 Only looks up to the top of the repository unless an 881 Only looks up to the top of the repository unless an
844 'inherit-review-settings-ok' file exists in the root of the repository. 882 'inherit-review-settings-ok' file exists in the root of the repository.
845 """ 883 """
846 inherit_ok_file = 'inherit-review-settings-ok' 884 inherit_ok_file = 'inherit-review-settings-ok'
847 cwd = os.getcwd() 885 cwd = os.getcwd()
(...skipping 247 matching lines...) Expand 10 before | Expand all | Expand 10 after
1095 1133
1096 def GerritUpload(options, args, cl): 1134 def GerritUpload(options, args, cl):
1097 """upload the current branch to gerrit.""" 1135 """upload the current branch to gerrit."""
1098 # We assume the remote called "origin" is the one we want. 1136 # We assume the remote called "origin" is the one we want.
1099 # It is probably not worthwhile to support different workflows. 1137 # It is probably not worthwhile to support different workflows.
1100 remote = 'origin' 1138 remote = 'origin'
1101 branch = 'master' 1139 branch = 'master'
1102 if options.target_branch: 1140 if options.target_branch:
1103 branch = options.target_branch 1141 branch = options.target_branch
1104 1142
1105 log_desc = options.message or CreateDescriptionFromLog(args) 1143 change_desc = ChangeDescription(
1106 if CHANGE_ID not in log_desc: 1144 options.message or CreateDescriptionFromLog(args))
1145 if not change_desc.description:
1146 print "Description is empty; aborting."
1147 return 1
1148 if CHANGE_ID not in change_desc.description:
Dirk Pranke 2013/04/09 00:07:52 I wonder if we should restructure this so that we
M-A Ruel 2013/04/11 00:01:38 I prefer to keep this out of this CL since it invo
1107 AddChangeIdToCommitMessage(options, args) 1149 AddChangeIdToCommitMessage(options, args)
1108 if options.reviewers: 1150 if options.reviewers:
1109 log_desc += '\nR=' + options.reviewers 1151 change_desc.update_reviewers(options.reviewers)
1110 change_desc = ChangeDescription(log_desc, options.reviewers)
1111 change_desc.ParseDescription()
1112 if change_desc.IsEmpty():
1113 print "Description is empty; aborting."
1114 return 1
1115 1152
1116 receive_options = [] 1153 receive_options = []
1117 cc = cl.GetCCList().split(',') 1154 cc = cl.GetCCList().split(',')
1118 if options.cc: 1155 if options.cc:
1119 cc += options.cc.split(',') 1156 cc += options.cc.split(',')
1120 cc = filter(None, cc) 1157 cc = filter(None, cc)
1121 if cc: 1158 if cc:
1122 receive_options += ['--cc=' + email for email in cc] 1159 receive_options += ['--cc=' + email for email in cc]
1123 if change_desc.reviewers: 1160 if change_desc.get_reviewers():
1124 reviewers = filter(None, change_desc.reviewers.split(',')) 1161 receive_options.extend(
1125 if reviewers: 1162 '--reviewer=' + email for email in change_desc.get_reviewers())
1126 receive_options += ['--reviewer=' + email for email in reviewers]
1127 1163
1128 git_command = ['push'] 1164 git_command = ['push']
1129 if receive_options: 1165 if receive_options:
1130 git_command.append('--receive-pack=git receive-pack %s' % 1166 git_command.append('--receive-pack=git receive-pack %s' %
1131 ' '.join(receive_options)) 1167 ' '.join(receive_options))
1132 git_command += [remote, 'HEAD:refs/for/' + branch] 1168 git_command += [remote, 'HEAD:refs/for/' + branch]
1133 RunGit(git_command) 1169 RunGit(git_command)
1134 # TODO(ukai): parse Change-Id: and set issue number? 1170 # TODO(ukai): parse Change-Id: and set issue number?
1135 return 0 1171 return 0
1136 1172
(...skipping 15 matching lines...) Expand all
1152 # for upload.py. Soon this will be changed to set the --message option. 1188 # for upload.py. Soon this will be changed to set the --message option.
1153 # Will wait until people are used to typing -t instead of -m. 1189 # Will wait until people are used to typing -t instead of -m.
1154 upload_args.extend(['--title', options.message]) 1190 upload_args.extend(['--title', options.message])
1155 upload_args.extend(['--issue', str(cl.GetIssue())]) 1191 upload_args.extend(['--issue', str(cl.GetIssue())])
1156 print ("This branch is associated with issue %s. " 1192 print ("This branch is associated with issue %s. "
1157 "Adding patch to that issue." % cl.GetIssue()) 1193 "Adding patch to that issue." % cl.GetIssue())
1158 else: 1194 else:
1159 if options.title: 1195 if options.title:
1160 upload_args.extend(['--title', options.title]) 1196 upload_args.extend(['--title', options.title])
1161 message = options.title or options.message or CreateDescriptionFromLog(args) 1197 message = options.title or options.message or CreateDescriptionFromLog(args)
1162 change_desc = ChangeDescription(message, options.reviewers) 1198 change_desc = ChangeDescription(message)
1199 if options.reviewers:
1200 change_desc.update_reviewers(options.reviewers)
1163 if not options.force: 1201 if not options.force:
1164 change_desc.Prompt() 1202 change_desc.prompt()
1165 change_desc.ParseDescription()
1166 1203
1167 if change_desc.IsEmpty(): 1204 if not change_desc.description:
1168 print "Description is empty; aborting." 1205 print "Description is empty; aborting."
1169 return 1 1206 return 1
1170 1207
1171 upload_args.extend(['--message', change_desc.description]) 1208 upload_args.extend(['--message', change_desc.description])
1172 if change_desc.reviewers: 1209 if change_desc.get_reviewers():
1173 upload_args.extend(['--reviewers', change_desc.reviewers]) 1210 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
1174 if options.send_mail: 1211 if options.send_mail:
1175 if not change_desc.reviewers: 1212 if not change_desc.get_reviewers():
1176 DieWithError("Must specify reviewers to send email.") 1213 DieWithError("Must specify reviewers to send email.")
1177 upload_args.append('--send_mail') 1214 upload_args.append('--send_mail')
1178 cc = ','.join(filter(None, (cl.GetCCList(), options.cc))) 1215 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
1179 if cc: 1216 if cc:
1180 upload_args.extend(['--cc', cc]) 1217 upload_args.extend(['--cc', cc])
1181 1218
1182 upload_args.extend(['--git_similarity', str(options.similarity)]) 1219 upload_args.extend(['--git_similarity', str(options.similarity)])
1183 if not options.find_copies: 1220 if not options.find_copies:
1184 upload_args.extend(['--git_no_find_copies']) 1221 upload_args.extend(['--git_no_find_copies'])
1185 1222
(...skipping 223 matching lines...) Expand 10 before | Expand all | Expand 10 after
1409 elif 'unknown' == status: 1446 elif 'unknown' == status:
1410 print('Unable to determine tree status. Please verify manually and ' 1447 print('Unable to determine tree status. Please verify manually and '
1411 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.') 1448 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
1412 else: 1449 else:
1413 breakpad.SendStack( 1450 breakpad.SendStack(
1414 'GitClHooksBypassedCommit', 1451 'GitClHooksBypassedCommit',
1415 'Issue %s/%s bypassed hook when committing' % 1452 'Issue %s/%s bypassed hook when committing' %
1416 (cl.GetRietveldServer(), cl.GetIssue()), 1453 (cl.GetRietveldServer(), cl.GetIssue()),
1417 verbose=False) 1454 verbose=False)
1418 1455
1419 description = options.message 1456 change_desc = ChangeDescription(options.message)
1420 if not description and cl.GetIssue(): 1457 if not change_desc.description and cl.GetIssue():
1421 description = cl.GetDescription() 1458 change_desc = ChangeDescription(cl.GetDescription())
1422 1459
1423 if not description: 1460 if not change_desc.description:
1424 if not cl.GetIssue() and options.bypass_hooks: 1461 if not cl.GetIssue() and options.bypass_hooks:
1425 description = CreateDescriptionFromLog([base_branch]) 1462 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
1426 else: 1463 else:
1427 print 'No description set.' 1464 print 'No description set.'
1428 print 'Visit %s/edit to set it.' % (cl.GetIssueURL()) 1465 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1429 return 1 1466 return 1
1430 1467
1468 # Keep a separate copy for the commit message.
Dirk Pranke 2013/04/09 00:07:52 Can you clarify *why* we need a separate copy here
M-A Ruel 2013/04/11 00:01:38 Done.
1469 commit_desc = ChangeDescription(change_desc.description)
1431 if cl.GetIssue(): 1470 if cl.GetIssue():
1432 description += "\n\nReview URL: %s" % cl.GetIssueURL() 1471 commit_desc.append_line('Review URL: %s' % cl.GetIssueURL())
1472 if options.contributor:
1473 commit_desc.append_line('Patch from %s.' % options.contributor)
1433 1474
1434 if options.contributor: 1475 print 'Description:', repr(commit_desc.description)
1435 description += "\nPatch from %s." % options.contributor
1436 print 'Description:', repr(description)
1437 1476
1438 branches = [base_branch, cl.GetBranchRef()] 1477 branches = [base_branch, cl.GetBranchRef()]
1439 if not options.force: 1478 if not options.force:
1440 print_stats(options.similarity, options.find_copies, branches) 1479 print_stats(options.similarity, options.find_copies, branches)
1441 ask_for_data('About to commit; enter to confirm.') 1480 ask_for_data('About to commit; enter to confirm.')
1442 1481
1443 # We want to squash all this branch's commits into one commit with the proper 1482 # We want to squash all this branch's commits into one commit with the proper
1444 # description. We do this by doing a "reset --soft" to the base branch (which 1483 # description. We do this by doing a "reset --soft" to the base branch (which
1445 # keeps the working copy the same), then dcommitting that. If origin/master 1484 # keeps the working copy the same), then dcommitting that. If origin/master
1446 # has a submodule merge commit, we'll also need to cherry-pick the squashed 1485 # has a submodule merge commit, we'll also need to cherry-pick the squashed
(...skipping 15 matching lines...) Expand all
1462 os.chdir(rel_base_path) 1501 os.chdir(rel_base_path)
1463 1502
1464 # Stuff our change into the merge branch. 1503 # Stuff our change into the merge branch.
1465 # We wrap in a try...finally block so if anything goes wrong, 1504 # We wrap in a try...finally block so if anything goes wrong,
1466 # we clean up the branches. 1505 # we clean up the branches.
1467 retcode = -1 1506 retcode = -1
1468 try: 1507 try:
1469 RunGit(['checkout', '-q', '-b', MERGE_BRANCH]) 1508 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1470 RunGit(['reset', '--soft', base_branch]) 1509 RunGit(['reset', '--soft', base_branch])
1471 if options.contributor: 1510 if options.contributor:
1472 RunGit(['commit', '--author', options.contributor, '-m', description]) 1511 RunGit(
1512 [
1513 'commit', '--author', options.contributor,
1514 '-m', commit_desc.description,
1515 ])
1473 else: 1516 else:
1474 RunGit(['commit', '-m', description]) 1517 RunGit(['commit', '-m', commit_desc.description])
1475 if base_has_submodules: 1518 if base_has_submodules:
1476 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip() 1519 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1477 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head]) 1520 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1478 RunGit(['checkout', CHERRY_PICK_BRANCH]) 1521 RunGit(['checkout', CHERRY_PICK_BRANCH])
1479 RunGit(['cherry-pick', cherry_pick_commit]) 1522 RunGit(['cherry-pick', cherry_pick_commit])
1480 if cmd == 'push': 1523 if cmd == 'push':
1481 # push the merge branch. 1524 # push the merge branch.
1482 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch()) 1525 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
1483 retcode, output = RunGitWithCode( 1526 retcode, output = RunGitWithCode(
1484 ['push', '--porcelain', remote, 'HEAD:%s' % branch]) 1527 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
(...skipping 18 matching lines...) Expand all
1503 for l in output.splitlines(False)) 1546 for l in output.splitlines(False))
1504 match = filter(None, match) 1547 match = filter(None, match)
1505 if len(match) != 1: 1548 if len(match) != 1:
1506 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" % 1549 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1507 output) 1550 output)
1508 revision = match[0].group(2) 1551 revision = match[0].group(2)
1509 else: 1552 else:
1510 return 1 1553 return 1
1511 viewvc_url = settings.GetViewVCUrl() 1554 viewvc_url = settings.GetViewVCUrl()
1512 if viewvc_url and revision: 1555 if viewvc_url and revision:
1513 cl.description += ('\n\nCommitted: ' + viewvc_url + revision) 1556 change_desc.append_line('Committed: ' + viewvc_url + revision)
1514 elif revision: 1557 elif revision:
1515 cl.description += ('\n\nCommitted: ' + revision) 1558 change_desc.append_line('Committed: ' + revision)
1516 print ('Closing issue ' 1559 print ('Closing issue '
1517 '(you may be prompted for your codereview password)...') 1560 '(you may be prompted for your codereview password)...')
1518 cl.UpdateDescription(cl.description) 1561 cl.UpdateDescription(change_desc.description)
1519 cl.CloseIssue() 1562 cl.CloseIssue()
1520 props = cl.RpcServer().get_issue_properties(cl.GetIssue(), False) 1563 props = cl.RpcServer().get_issue_properties(cl.GetIssue(), False)
1521 patch_num = len(props['patchsets']) 1564 patch_num = len(props['patchsets'])
1522 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision) 1565 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
1523 comment += ' (presubmit successful).' if not options.bypass_hooks else '.' 1566 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1524 cl.RpcServer().add_comment(cl.GetIssue(), comment) 1567 cl.RpcServer().add_comment(cl.GetIssue(), comment)
1525 cl.SetIssue(0) 1568 cl.SetIssue(0)
1526 1569
1527 if retcode == 0: 1570 if retcode == 0:
1528 hook = POSTUPSTREAM_HOOK_PATTERN % cmd 1571 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
(...skipping 376 matching lines...) Expand 10 before | Expand all | Expand 10 after
1905 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e))) 1948 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1906 1949
1907 # Not a known command. Default to help. 1950 # Not a known command. Default to help.
1908 GenUsage(parser, 'help') 1951 GenUsage(parser, 'help')
1909 return CMDhelp(parser, argv) 1952 return CMDhelp(parser, argv)
1910 1953
1911 1954
1912 if __name__ == '__main__': 1955 if __name__ == '__main__':
1913 fix_encoding.fix_encoding() 1956 fix_encoding.fix_encoding()
1914 sys.exit(main(sys.argv[1:])) 1957 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