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

Side by Side Diff: git_cl/git_cl.py

Issue 6719004: refactor parsing of change descriptions, fix TBR= (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: rebase to head, fix bugs Created 9 years, 9 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 | Annotate | Revision Log
« no previous file with comments | « gclient_utils.py ('k') | presubmit_support.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/python 1 #!/usr/bin/python
2 # git-cl -- a git-command for integrating reviews on Rietveld 2 # git-cl -- a git-command for integrating reviews on Rietveld
3 # Copyright (C) 2008 Evan Martin <martine@danga.com> 3 # Copyright (C) 2008 Evan Martin <martine@danga.com>
4 4
5 import errno 5 import errno
6 import logging 6 import logging
7 import optparse 7 import optparse
8 import os 8 import os
9 import re 9 import re
10 import subprocess 10 import subprocess
11 import sys 11 import sys
12 import tempfile
13 import textwrap 12 import textwrap
14 import urlparse 13 import urlparse
15 import urllib2 14 import urllib2
16 15
17 try: 16 try:
18 import readline # pylint: disable=W0611 17 import readline # pylint: disable=W0611
19 except ImportError: 18 except ImportError:
20 pass 19 pass
21 20
22 # TODO(dpranke): don't use relative import. 21 # TODO(dpranke): don't use relative import.
23 import upload # pylint: disable=W0403 22 import upload # pylint: disable=W0403
24 try: 23
25 # TODO(dpranke): We wrap this in a try block for a limited form of 24 # TODO(dpranke): move this file up a directory so we don't need this.
26 # backwards-compatibility with older versions of git-cl that weren't 25 depot_tools_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
27 # dependent on depot_tools. This version should still work outside of 26 sys.path.append(depot_tools_path)
28 # depot_tools as long as --bypass-hooks is used. We should remove this 27
29 # once this has baked for a while and things seem safe. 28 import breakpad # pylint: disable=W0611
30 depot_tools_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 29
31 sys.path.append(depot_tools_path) 30 import presubmit_support
32 import breakpad # pylint: disable=W0611 31 import scm
33 except ImportError: 32 import watchlists
34 pass 33
35 34
36 DEFAULT_SERVER = 'http://codereview.appspot.com' 35 DEFAULT_SERVER = 'http://codereview.appspot.com'
37 POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s' 36 POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
38 DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup' 37 DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
39 38
40 def DieWithError(message): 39 def DieWithError(message):
41 print >> sys.stderr, message 40 print >> sys.stderr, message
42 sys.exit(1) 41 sys.exit(1)
43 42
44 43
(...skipping 281 matching lines...) Expand 10 before | Expand all | Expand 10 after
326 else: 325 else:
327 self.branch = None 326 self.branch = None
328 self.rietveld_server = None 327 self.rietveld_server = None
329 self.upstream_branch = None 328 self.upstream_branch = None
330 self.has_issue = False 329 self.has_issue = False
331 self.issue = None 330 self.issue = None
332 self.has_description = False 331 self.has_description = False
333 self.description = None 332 self.description = None
334 self.has_patchset = False 333 self.has_patchset = False
335 self.patchset = None 334 self.patchset = None
335 self.tbr = False
336 336
337 def GetBranch(self): 337 def GetBranch(self):
338 """Returns the short branch name, e.g. 'master'.""" 338 """Returns the short branch name, e.g. 'master'."""
339 if not self.branch: 339 if not self.branch:
340 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip() 340 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
341 self.branch = ShortBranchName(self.branchref) 341 self.branch = ShortBranchName(self.branchref)
342 return self.branch 342 return self.branch
343 343
344 def GetBranchRef(self): 344 def GetBranchRef(self):
345 """Returns the full branch name, e.g. 'refs/heads/master'.""" 345 """Returns the full branch name, e.g. 'refs/heads/master'."""
(...skipping 182 matching lines...) Expand 10 before | Expand all | Expand 10 after
528 528
529 SetProperty(settings.GetCCList(), 'CC list', 'cc') 529 SetProperty(settings.GetCCList(), 'CC list', 'cc')
530 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL', 530 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
531 'tree-status-url') 531 'tree-status-url')
532 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url') 532 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url')
533 533
534 # TODO: configure a default branch to diff against, rather than this 534 # TODO: configure a default branch to diff against, rather than this
535 # svn-based hackery. 535 # svn-based hackery.
536 536
537 537
538 class ChangeDescription(object):
539 """Contains a parsed form of the change description."""
540 def __init__(self, subject, log_desc, reviewers):
541 self.subject = subject
542 self.log_desc = log_desc
543 self.reviewers = reviewers
544 self.description = self.log_desc
545
546 def Update(self):
547 initial_text = """# Enter a description of the change.
548 # This will displayed on the codereview site.
549 # The first line will also be used as the subject of the review.
550 """
551 initial_text += self.description
552 if 'R=' not in self.description and self.reviewers:
553 initial_text += '\nR=' + self.reviewers
554 if 'BUG=' not in self.description:
555 initial_text += '\nBUG='
556 if 'TEST=' not in self.description:
557 initial_text += '\nTEST='
558 self._ParseDescription(UserEditedLog(initial_text))
559
560 def _ParseDescription(self, description):
561 if not description:
562 self.description = description
563 return
564
565 parsed_lines = []
566 reviewers_regexp = re.compile('\s*R=(.+)')
567 reviewers = ''
568 subject = ''
569 for l in description.splitlines():
570 if not subject:
571 subject = l
572 matched_reviewers = reviewers_regexp.match(l)
573 if matched_reviewers:
574 reviewers = matched_reviewers.group(1)
575 parsed_lines.append(l)
576
577 self.description = '\n'.join(parsed_lines) + '\n'
578 self.subject = subject
579 self.reviewers = reviewers
580
581 def IsEmpty(self):
582 return not self.description
583
584
585 def FindCodereviewSettingsFile(filename='codereview.settings'): 538 def FindCodereviewSettingsFile(filename='codereview.settings'):
586 """Finds the given file starting in the cwd and going up. 539 """Finds the given file starting in the cwd and going up.
587 540
588 Only looks up to the top of the repository unless an 541 Only looks up to the top of the repository unless an
589 'inherit-review-settings-ok' file exists in the root of the repository. 542 'inherit-review-settings-ok' file exists in the root of the repository.
590 """ 543 """
591 inherit_ok_file = 'inherit-review-settings-ok' 544 inherit_ok_file = 'inherit-review-settings-ok'
592 cwd = os.getcwd() 545 cwd = os.getcwd()
593 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip()) 546 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
594 if os.path.isfile(os.path.join(root, inherit_ok_file)): 547 if os.path.isfile(os.path.join(root, inherit_ok_file)):
(...skipping 129 matching lines...) Expand 10 before | Expand all | Expand 10 after
724 log_args = [args[0] + '..'] 677 log_args = [args[0] + '..']
725 elif len(args) == 1 and args[0].endswith('...'): 678 elif len(args) == 1 and args[0].endswith('...'):
726 log_args = [args[0][:-1]] 679 log_args = [args[0][:-1]]
727 elif len(args) == 2: 680 elif len(args) == 2:
728 log_args = [args[0] + '..' + args[1]] 681 log_args = [args[0] + '..' + args[1]]
729 else: 682 else:
730 log_args = args[:] # Hope for the best! 683 log_args = args[:] # Hope for the best!
731 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args) 684 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
732 685
733 686
734 def UserEditedLog(starting_text):
735 """Given some starting text, let the user edit it and return the result."""
736 editor = os.getenv('EDITOR', 'vi')
737
738 (file_handle, filename) = tempfile.mkstemp()
739 fileobj = os.fdopen(file_handle, 'w')
740 fileobj.write(starting_text)
741 fileobj.close()
742
743 # Open up the default editor in the system to get the CL description.
744 try:
745 cmd = '%s %s' % (editor, filename)
746 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
747 # Msysgit requires the usage of 'env' to be present.
748 cmd = 'env ' + cmd
749 # shell=True to allow the shell to handle all forms of quotes in $EDITOR.
750 subprocess.check_call(cmd, shell=True)
751 fileobj = open(filename)
752 text = fileobj.read()
753 fileobj.close()
754 finally:
755 os.remove(filename)
756
757 if not text:
758 return
759
760 stripcomment_re = re.compile(r'^#.*$', re.MULTILINE)
761 return stripcomment_re.sub('', text).strip()
762
763
764 def ConvertToInteger(inputval): 687 def ConvertToInteger(inputval):
765 """Convert a string to integer, but returns either an int or None.""" 688 """Convert a string to integer, but returns either an int or None."""
766 try: 689 try:
767 return int(inputval) 690 return int(inputval)
768 except (TypeError, ValueError): 691 except (TypeError, ValueError):
769 return None 692 return None
770 693
771 694
695 class GitChangeDescription(presubmit_support.ChangeDescription):
696 def UserEdit(self):
697 header = (
698 "# Enter a description of the change.\n"
699 "# This will displayed on the codereview site.\n"
700 "# The first line will also be used as the subject of the review.\n"
701 "\n")
702 edited_text = self.editor(header + self.EditableDescription())
703 stripcomment_re = re.compile(r'^#.*$', re.MULTILINE)
704 self.Parse(stripcomment_re.sub('', edited_text).strip())
705
706
772 def RunHook(committing, upstream_branch, rietveld_server, tbr, may_prompt): 707 def RunHook(committing, upstream_branch, rietveld_server, tbr, may_prompt):
773 """Calls sys.exit() if the hook fails; returns a HookResults otherwise.""" 708 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
774 import presubmit_support
775 import scm
776 import watchlists
777
778 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() 709 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip()
779 if not root: 710 if not root:
780 root = '.' 711 root = '.'
781 absroot = os.path.abspath(root) 712 absroot = os.path.abspath(root)
782 if not root: 713 if not root:
783 raise Exception('Could not get root directory.') 714 raise Exception('Could not get root directory.')
784 715
785 # We use the sha1 of HEAD as a name of this change. 716 # We use the sha1 of HEAD as a name of this change.
786 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip() 717 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
787 files = scm.GIT.CaptureStatus([root], upstream_branch) 718 files = scm.GIT.CaptureStatus([root], upstream_branch)
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after
836 cl = Changelist() 767 cl = Changelist()
837 if args: 768 if args:
838 base_branch = args[0] 769 base_branch = args[0]
839 else: 770 else:
840 # Default to diffing against the "upstream" branch. 771 # Default to diffing against the "upstream" branch.
841 base_branch = cl.GetUpstreamBranch() 772 base_branch = cl.GetUpstreamBranch()
842 773
843 if options.upload: 774 if options.upload:
844 print '*** Presubmit checks for UPLOAD would report: ***' 775 print '*** Presubmit checks for UPLOAD would report: ***'
845 RunHook(committing=False, upstream_branch=base_branch, 776 RunHook(committing=False, upstream_branch=base_branch,
846 rietveld_server=cl.GetRietveldServer(), tbr=False, 777 rietveld_server=cl.GetRietveldServer(), tbr=cl.tbr,
847 may_prompt=False) 778 may_prompt=False)
848 return 0 779 return 0
849 else: 780 else:
850 print '*** Presubmit checks for DCOMMIT would report: ***' 781 print '*** Presubmit checks for DCOMMIT would report: ***'
851 RunHook(committing=True, upstream_branch=base_branch, 782 RunHook(committing=True, upstream_branch=base_branch,
852 rietveld_server=cl.GetRietveldServer, tbr=False, 783 rietveld_server=cl.GetRietveldServer, tbr=cl.tbr,
853 may_prompt=False) 784 may_prompt=False)
854 return 0 785 return 0
855 786
856 787
857 @usage('[args to "git diff"]') 788 @usage('[args to "git diff"]')
858 def CMDupload(parser, args): 789 def CMDupload(parser, args):
859 """upload the current changelist to codereview""" 790 """upload the current changelist to codereview"""
860 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks', 791 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
861 help='bypass upload presubmit hook') 792 help='bypass upload presubmit hook')
862 parser.add_option('-f', action='store_true', dest='force', 793 parser.add_option('-f', action='store_true', dest='force',
(...skipping 22 matching lines...) Expand all
885 816
886 cl = Changelist() 817 cl = Changelist()
887 if args: 818 if args:
888 base_branch = args[0] 819 base_branch = args[0]
889 else: 820 else:
890 # Default to diffing against the "upstream" branch. 821 # Default to diffing against the "upstream" branch.
891 base_branch = cl.GetUpstreamBranch() 822 base_branch = cl.GetUpstreamBranch()
892 args = [base_branch + "..."] 823 args = [base_branch + "..."]
893 824
894 if not options.bypass_hooks and not options.force: 825 if not options.bypass_hooks and not options.force:
895 hook_results = RunHook(committing=False, upstream_branch=base_branch, 826 RunHook(committing=False, upstream_branch=base_branch,
896 rietveld_server=cl.GetRietveldServer(), tbr=False, 827 rietveld_server=cl.GetRietveldServer(), tbr=cl.tbr,
897 may_prompt=True) 828 may_prompt=True)
898 if not options.reviewers and hook_results.reviewers:
899 options.reviewers = hook_results.reviewers
900
901 829
902 # --no-ext-diff is broken in some versions of Git, so try to work around 830 # --no-ext-diff is broken in some versions of Git, so try to work around
903 # this by overriding the environment (but there is still a problem if the 831 # this by overriding the environment (but there is still a problem if the
904 # git config key "diff.external" is used). 832 # git config key "diff.external" is used).
905 env = os.environ.copy() 833 env = os.environ.copy()
906 if 'GIT_EXTERNAL_DIFF' in env: 834 if 'GIT_EXTERNAL_DIFF' in env:
907 del env['GIT_EXTERNAL_DIFF'] 835 del env['GIT_EXTERNAL_DIFF']
908 subprocess.call(['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, 836 subprocess.call(['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args,
909 env=env) 837 env=env)
910 838
(...skipping 12 matching lines...) Expand all
923 change_desc = None 851 change_desc = None
924 852
925 if cl.GetIssue(): 853 if cl.GetIssue():
926 if options.message: 854 if options.message:
927 upload_args.extend(['--message', options.message]) 855 upload_args.extend(['--message', options.message])
928 upload_args.extend(['--issue', cl.GetIssue()]) 856 upload_args.extend(['--issue', cl.GetIssue()])
929 print ("This branch is associated with issue %s. " 857 print ("This branch is associated with issue %s. "
930 "Adding patch to that issue." % cl.GetIssue()) 858 "Adding patch to that issue." % cl.GetIssue())
931 else: 859 else:
932 log_desc = CreateDescriptionFromLog(args) 860 log_desc = CreateDescriptionFromLog(args)
933 change_desc = ChangeDescription(options.message, log_desc, 861 change_desc = GitChangeDescription(subject=options.message,
934 options.reviewers) 862 description=log_desc, reviewers=options.reviewers, tbr=cl.tbr)
935 if not options.from_logs: 863 if not options.from_logs and (not options.force):
936 change_desc.Update() 864 change_desc.UserEdit()
937 865
938 if change_desc.IsEmpty(): 866 if change_desc.IsEmpty():
939 print "Description is empty; aborting." 867 print "Description is empty; aborting."
940 return 1 868 return 1
941 869
942 upload_args.extend(['--message', change_desc.subject]) 870 upload_args.extend(['--message', change_desc.subject])
943 upload_args.extend(['--description', change_desc.description]) 871 upload_args.extend(['--description', change_desc.description])
944 if change_desc.reviewers: 872 if change_desc.reviewers:
945 upload_args.extend(['--reviewers', change_desc.reviewers]) 873 upload_args.extend(['--reviewers', change_desc.reviewers])
946 cc = ','.join(filter(None, (settings.GetCCList(), options.cc))) 874 cc = ','.join(filter(None, (settings.GetCCList(), options.cc)))
(...skipping 90 matching lines...) Expand 10 before | Expand all | Expand 10 after
1037 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch]) 965 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
1038 if extra_commits: 966 if extra_commits:
1039 print ('This branch has %d additional commits not upstreamed yet.' 967 print ('This branch has %d additional commits not upstreamed yet.'
1040 % len(extra_commits.splitlines())) 968 % len(extra_commits.splitlines()))
1041 print ('Upstream "%s" or rebase this branch on top of the upstream trunk ' 969 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1042 'before attempting to %s.' % (base_branch, cmd)) 970 'before attempting to %s.' % (base_branch, cmd))
1043 return 1 971 return 1
1044 972
1045 if not options.bypass_hooks and not options.force: 973 if not options.bypass_hooks and not options.force:
1046 RunHook(committing=True, upstream_branch=base_branch, 974 RunHook(committing=True, upstream_branch=base_branch,
1047 rietveld_server=cl.GetRietveldServer(), tbr=options.tbr, 975 rietveld_server=cl.GetRietveldServer(), tbr=(cl.tbr or options.tbr),
1048 may_prompt=True) 976 may_prompt=True)
1049 977
1050 if cmd == 'dcommit': 978 if cmd == 'dcommit':
1051 # Check the tree status if the tree status URL is set. 979 # Check the tree status if the tree status URL is set.
1052 status = GetTreeStatus() 980 status = GetTreeStatus()
1053 if 'closed' == status: 981 if 'closed' == status:
1054 print ('The tree is closed. Please wait for it to reopen. Use ' 982 print ('The tree is closed. Please wait for it to reopen. Use '
1055 '"git cl dcommit -f" to commit on a closed tree.') 983 '"git cl dcommit -f" to commit on a closed tree.')
1056 return 1 984 return 1
1057 elif 'unknown' == status: 985 elif 'unknown' == status:
(...skipping 18 matching lines...) Expand all
1076 print 'Visit %s/edit to set it.' % (cl.GetIssueURL()) 1004 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1077 return 1 1005 return 1
1078 1006
1079 description += "\n\nReview URL: %s" % cl.GetIssueURL() 1007 description += "\n\nReview URL: %s" % cl.GetIssueURL()
1080 else: 1008 else:
1081 if not description: 1009 if not description:
1082 # Submitting TBR. See if there's already a description in Rietveld, else 1010 # Submitting TBR. See if there's already a description in Rietveld, else
1083 # create a template description. Eitherway, give the user a chance to edit 1011 # create a template description. Eitherway, give the user a chance to edit
1084 # it to fill in the TBR= field. 1012 # it to fill in the TBR= field.
1085 if cl.GetIssue(): 1013 if cl.GetIssue():
1086 description = cl.GetDescription() 1014 change_desc = GitChangeDescription(description=cl.GetDescription())
1087 1015
1088 # TODO(dpranke): Update to use ChangeDescription object.
1089 if not description: 1016 if not description:
1090 description = """# Enter a description of the change. 1017 log_desc = CreateDescriptionFromLog(args)
1091 # This will be used as the change log for the commit. 1018 change_desc = GitChangeDescription(description=log_desc, tbr=True)
1092 1019
1093 """ 1020 if not options.force:
1094 description += CreateDescriptionFromLog(args) 1021 change_desc.UserEdit()
1095 1022 description = change_desc.description
1096 description = UserEditedLog(description + '\nTBR=')
1097 1023
1098 if not description: 1024 if not description:
1099 print "Description empty; aborting." 1025 print "Description empty; aborting."
1100 return 1 1026 return 1
1101 1027
1102 if options.contributor: 1028 if options.contributor:
1103 if not re.match('^.*\s<\S+@\S+>$', options.contributor): 1029 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1104 print "Please provide contibutor as 'First Last <email@example.com>'" 1030 print "Please provide contibutor as 'First Last <email@example.com>'"
1105 return 1 1031 return 1
1106 description += "\nPatch from %s." % options.contributor 1032 description += "\nPatch from %s." % options.contributor
(...skipping 316 matching lines...) Expand 10 before | Expand all | Expand 10 after
1423 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith ' 1349 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1424 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e))) 1350 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1425 1351
1426 # Not a known command. Default to help. 1352 # Not a known command. Default to help.
1427 GenUsage(parser, 'help') 1353 GenUsage(parser, 'help')
1428 return CMDhelp(parser, argv) 1354 return CMDhelp(parser, argv)
1429 1355
1430 1356
1431 if __name__ == '__main__': 1357 if __name__ == '__main__':
1432 sys.exit(main(sys.argv[1:])) 1358 sys.exit(main(sys.argv[1:]))
OLDNEW
« no previous file with comments | « gclient_utils.py ('k') | presubmit_support.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698