OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2011 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 errno | |
11 import logging | 10 import logging |
12 import optparse | 11 import optparse |
13 import os | 12 import os |
14 import re | 13 import re |
15 import subprocess | |
16 import sys | 14 import sys |
17 import tempfile | 15 import tempfile |
18 import textwrap | 16 import textwrap |
19 import urlparse | 17 import urlparse |
20 import urllib2 | 18 import urllib2 |
21 | 19 |
22 try: | 20 try: |
23 import readline # pylint: disable=F0401,W0611 | 21 import readline # pylint: disable=F0401,W0611 |
24 except ImportError: | 22 except ImportError: |
25 pass | 23 pass |
26 | 24 |
27 try: | 25 try: |
28 import simplejson as json # pylint: disable=F0401 | 26 import simplejson as json # pylint: disable=F0401 |
29 except ImportError: | 27 except ImportError: |
30 try: | 28 try: |
31 import json # pylint: disable=F0401 | 29 import json # pylint: disable=F0401 |
32 except ImportError: | 30 except ImportError: |
33 # Fall back to the packaged version. | 31 # Fall back to the packaged version. |
34 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party')) | 32 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party')) |
35 import simplejson as json # pylint: disable=F0401 | 33 import simplejson as json # pylint: disable=F0401 |
36 | 34 |
37 | 35 |
38 from third_party import upload | 36 from third_party import upload |
39 import breakpad # pylint: disable=W0611 | 37 import breakpad # pylint: disable=W0611 |
40 import fix_encoding | 38 import fix_encoding |
41 import presubmit_support | 39 import presubmit_support |
42 import rietveld | 40 import rietveld |
43 import scm | 41 import scm |
| 42 import subprocess2 |
44 import watchlists | 43 import watchlists |
45 | 44 |
46 | 45 |
47 | |
48 DEFAULT_SERVER = 'http://codereview.appspot.com' | 46 DEFAULT_SERVER = 'http://codereview.appspot.com' |
49 POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s' | 47 POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s' |
50 DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup' | 48 DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup' |
51 | 49 |
52 | 50 |
53 def DieWithError(message): | 51 def DieWithError(message): |
54 print >> sys.stderr, message | 52 print >> sys.stderr, message |
55 sys.exit(1) | 53 sys.exit(1) |
56 | 54 |
57 | 55 |
58 def Popen(cmd, **kwargs): | 56 def RunCommand(args, error_ok=False, error_message=None, **kwargs): |
59 """Wrapper for subprocess.Popen() that logs and watch for cygwin issues""" | |
60 logging.debug('Popen: ' + ' '.join(cmd)) | |
61 try: | 57 try: |
62 return subprocess.Popen(cmd, **kwargs) | 58 return subprocess2.check_output(args, **kwargs) |
63 except OSError, e: | 59 except subprocess2.CalledProcessError, e: |
64 if e.errno == errno.EAGAIN and sys.platform == 'cygwin': | 60 if not error_ok: |
65 DieWithError( | 61 DieWithError( |
66 'Visit ' | 62 'Command "%s" failed.\n%s' % ( |
67 'http://code.google.com/p/chromium/wiki/CygwinDllRemappingFailure to ' | 63 ' '.join(args), error_message or e.stdout or '')) |
68 'learn how to fix this error; you need to rebase your cygwin dlls') | 64 return e.stdout |
69 raise | |
70 | |
71 | |
72 def RunCommand(cmd, error_ok=False, error_message=None, | |
73 redirect_stdout=True, swallow_stderr=False, **kwargs): | |
74 if redirect_stdout: | |
75 stdout = subprocess.PIPE | |
76 else: | |
77 stdout = None | |
78 if swallow_stderr: | |
79 stderr = subprocess.PIPE | |
80 else: | |
81 stderr = None | |
82 proc = Popen(cmd, stdout=stdout, stderr=stderr, **kwargs) | |
83 output = proc.communicate()[0] | |
84 if not error_ok and proc.returncode != 0: | |
85 DieWithError('Command "%s" failed.\n' % (' '.join(cmd)) + | |
86 (error_message or output or '')) | |
87 return output | |
88 | 65 |
89 | 66 |
90 def RunGit(args, **kwargs): | 67 def RunGit(args, **kwargs): |
91 cmd = ['git'] + args | 68 """Returns stdout.""" |
92 return RunCommand(cmd, **kwargs) | 69 return RunCommand(['git'] + args, **kwargs) |
93 | 70 |
94 | 71 |
95 def RunGitWithCode(args): | 72 def RunGitWithCode(args): |
96 proc = Popen(['git'] + args, stdout=subprocess.PIPE) | 73 """Returns return code and stdout.""" |
97 output = proc.communicate()[0] | 74 out, code = subprocess2.communicate(['git'] + args, stdout=subprocess2.PIPE) |
98 return proc.returncode, output | 75 return code, out[0] |
99 | 76 |
100 | 77 |
101 def usage(more): | 78 def usage(more): |
102 def hook(fn): | 79 def hook(fn): |
103 fn.usage_more = more | 80 fn.usage_more = more |
104 return fn | 81 return fn |
105 return hook | 82 return hook |
106 | 83 |
107 | 84 |
108 def ask_for_data(prompt): | 85 def ask_for_data(prompt): |
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
156 | 133 |
157 # Parse specs like "trunk/src:refs/remotes/origin/trunk". | 134 # Parse specs like "trunk/src:refs/remotes/origin/trunk". |
158 if fetch_suburl: | 135 if fetch_suburl: |
159 full_url = base_url + '/' + fetch_suburl | 136 full_url = base_url + '/' + fetch_suburl |
160 else: | 137 else: |
161 full_url = base_url | 138 full_url = base_url |
162 if full_url == url: | 139 if full_url == url: |
163 return as_ref | 140 return as_ref |
164 return None | 141 return None |
165 | 142 |
| 143 |
166 class Settings(object): | 144 class Settings(object): |
167 def __init__(self): | 145 def __init__(self): |
168 self.default_server = None | 146 self.default_server = None |
169 self.cc = None | 147 self.cc = None |
170 self.root = None | 148 self.root = None |
171 self.is_git_svn = None | 149 self.is_git_svn = None |
172 self.svn_branch = None | 150 self.svn_branch = None |
173 self.tree_status_url = None | 151 self.tree_status_url = None |
174 self.viewvc_url = None | 152 self.viewvc_url = None |
175 self.updated = False | 153 self.updated = False |
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
219 # 1) iterate through our branch history and find the svn URL. | 197 # 1) iterate through our branch history and find the svn URL. |
220 # 2) find the svn-remote that fetches from the URL. | 198 # 2) find the svn-remote that fetches from the URL. |
221 | 199 |
222 # regexp matching the git-svn line that contains the URL. | 200 # regexp matching the git-svn line that contains the URL. |
223 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE) | 201 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE) |
224 | 202 |
225 # We don't want to go through all of history, so read a line from the | 203 # We don't want to go through all of history, so read a line from the |
226 # pipe at a time. | 204 # pipe at a time. |
227 # The -100 is an arbitrary limit so we don't search forever. | 205 # The -100 is an arbitrary limit so we don't search forever. |
228 cmd = ['git', 'log', '-100', '--pretty=medium'] | 206 cmd = ['git', 'log', '-100', '--pretty=medium'] |
229 proc = Popen(cmd, stdout=subprocess.PIPE) | 207 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE) |
230 url = None | 208 url = None |
231 for line in proc.stdout: | 209 for line in proc.stdout: |
232 match = git_svn_re.match(line) | 210 match = git_svn_re.match(line) |
233 if match: | 211 if match: |
234 url = match.group(1) | 212 url = match.group(1) |
235 proc.stdout.close() # Cut pipe. | 213 proc.stdout.close() # Cut pipe. |
236 break | 214 break |
237 | 215 |
238 if url: | 216 if url: |
239 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$') | 217 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$') |
(...skipping 243 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
483 self.patchset = None | 461 self.patchset = None |
484 self.has_patchset = True | 462 self.has_patchset = True |
485 return self.patchset | 463 return self.patchset |
486 | 464 |
487 def SetPatchset(self, patchset): | 465 def SetPatchset(self, patchset): |
488 """Set this branch's patchset. If patchset=0, clears the patchset.""" | 466 """Set this branch's patchset. If patchset=0, clears the patchset.""" |
489 if patchset: | 467 if patchset: |
490 RunGit(['config', self._PatchsetSetting(), str(patchset)]) | 468 RunGit(['config', self._PatchsetSetting(), str(patchset)]) |
491 else: | 469 else: |
492 RunGit(['config', '--unset', self._PatchsetSetting()], | 470 RunGit(['config', '--unset', self._PatchsetSetting()], |
493 swallow_stderr=True, error_ok=True) | 471 stderr=subprocess2.PIPE, error_ok=True) |
494 self.has_patchset = False | 472 self.has_patchset = False |
495 | 473 |
496 def GetPatchSetDiff(self, issue): | 474 def GetPatchSetDiff(self, issue): |
497 patchset = self.RpcServer().get_issue_properties( | 475 patchset = self.RpcServer().get_issue_properties( |
498 int(issue), False)['patchsets'][-1] | 476 int(issue), False)['patchsets'][-1] |
499 return self.RpcServer().get( | 477 return self.RpcServer().get( |
500 '/download/issue%s_%s.diff' % (issue, patchset)) | 478 '/download/issue%s_%s.diff' % (issue, patchset)) |
501 | 479 |
502 def SetIssue(self, issue): | 480 def SetIssue(self, issue): |
503 """Set this branch's issue. If issue=0, clears the issue.""" | 481 """Set this branch's issue. If issue=0, clears the issue.""" |
(...skipping 340 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
844 fileobj.close() | 822 fileobj.close() |
845 | 823 |
846 # Open up the default editor in the system to get the CL description. | 824 # Open up the default editor in the system to get the CL description. |
847 try: | 825 try: |
848 cmd = '%s %s' % (editor, filename) | 826 cmd = '%s %s' % (editor, filename) |
849 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys': | 827 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys': |
850 # Msysgit requires the usage of 'env' to be present. | 828 # Msysgit requires the usage of 'env' to be present. |
851 cmd = 'env ' + cmd | 829 cmd = 'env ' + cmd |
852 # shell=True to allow the shell to handle all forms of quotes in $EDITOR. | 830 # shell=True to allow the shell to handle all forms of quotes in $EDITOR. |
853 try: | 831 try: |
854 subprocess.check_call(cmd, shell=True) | 832 subprocess2.check_call(cmd, shell=True) |
855 except subprocess.CalledProcessError, e: | 833 except subprocess2.CalledProcessError, e: |
856 DieWithError('Editor returned %d' % e.returncode) | 834 DieWithError('Editor returned %d' % e.returncode) |
857 fileobj = open(filename) | 835 fileobj = open(filename) |
858 text = fileobj.read() | 836 text = fileobj.read() |
859 fileobj.close() | 837 fileobj.close() |
860 finally: | 838 finally: |
861 os.remove(filename) | 839 os.remove(filename) |
862 | 840 |
863 if not text: | 841 if not text: |
864 return | 842 return |
865 | 843 |
(...skipping 82 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
948 if not options.reviewers and hook_results.reviewers: | 926 if not options.reviewers and hook_results.reviewers: |
949 options.reviewers = hook_results.reviewers | 927 options.reviewers = hook_results.reviewers |
950 | 928 |
951 | 929 |
952 # --no-ext-diff is broken in some versions of Git, so try to work around | 930 # --no-ext-diff is broken in some versions of Git, so try to work around |
953 # this by overriding the environment (but there is still a problem if the | 931 # this by overriding the environment (but there is still a problem if the |
954 # git config key "diff.external" is used). | 932 # git config key "diff.external" is used). |
955 env = os.environ.copy() | 933 env = os.environ.copy() |
956 if 'GIT_EXTERNAL_DIFF' in env: | 934 if 'GIT_EXTERNAL_DIFF' in env: |
957 del env['GIT_EXTERNAL_DIFF'] | 935 del env['GIT_EXTERNAL_DIFF'] |
958 subprocess.call(['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, | 936 subprocess2.call( |
959 env=env) | 937 ['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, env=env) |
960 | 938 |
961 upload_args = ['--assume_yes'] # Don't ask about untracked files. | 939 upload_args = ['--assume_yes'] # Don't ask about untracked files. |
962 upload_args.extend(['--server', cl.GetRietveldServer()]) | 940 upload_args.extend(['--server', cl.GetRietveldServer()]) |
963 if options.emulate_svn_auto_props: | 941 if options.emulate_svn_auto_props: |
964 upload_args.append('--emulate_svn_auto_props') | 942 upload_args.append('--emulate_svn_auto_props') |
965 if options.send_mail: | 943 if options.send_mail: |
966 if not options.reviewers: | 944 if not options.reviewers: |
967 DieWithError("Must specify reviewers to send email.") | 945 DieWithError("Must specify reviewers to send email.") |
968 upload_args.append('--send_mail') | 946 upload_args.append('--send_mail') |
969 if options.from_logs and not options.message: | 947 if options.from_logs and not options.message: |
(...skipping 159 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1129 | 1107 |
1130 if cl.GetIssue(): | 1108 if cl.GetIssue(): |
1131 description += "\n\nReview URL: %s" % cl.GetIssueURL() | 1109 description += "\n\nReview URL: %s" % cl.GetIssueURL() |
1132 | 1110 |
1133 if options.contributor: | 1111 if options.contributor: |
1134 description += "\nPatch from %s." % options.contributor | 1112 description += "\nPatch from %s." % options.contributor |
1135 print 'Description:', repr(description) | 1113 print 'Description:', repr(description) |
1136 | 1114 |
1137 branches = [base_branch, cl.GetBranchRef()] | 1115 branches = [base_branch, cl.GetBranchRef()] |
1138 if not options.force: | 1116 if not options.force: |
1139 subprocess.call(['git', 'diff', '--stat'] + branches) | 1117 subprocess2.call(['git', 'diff', '--stat'] + branches) |
1140 ask_for_data('About to commit; enter to confirm.') | 1118 ask_for_data('About to commit; enter to confirm.') |
1141 | 1119 |
1142 # We want to squash all this branch's commits into one commit with the | 1120 # We want to squash all this branch's commits into one commit with the |
1143 # proper description. | 1121 # proper description. |
1144 # We do this by doing a "reset --soft" to the base branch (which keeps | 1122 # We do this by doing a "reset --soft" to the base branch (which keeps |
1145 # the working copy the same), then dcommitting that. | 1123 # the working copy the same), then dcommitting that. |
1146 MERGE_BRANCH = 'git-cl-commit' | 1124 MERGE_BRANCH = 'git-cl-commit' |
1147 # Delete the merge branch if it already exists. | 1125 # Delete the merge branch if it already exists. |
1148 if RunGitWithCode(['show-ref', '--quiet', '--verify', | 1126 if RunGitWithCode(['show-ref', '--quiet', '--verify', |
1149 'refs/heads/' + MERGE_BRANCH])[0] == 0: | 1127 'refs/heads/' + MERGE_BRANCH])[0] == 0: |
(...skipping 116 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1266 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url) | 1244 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url) |
1267 if not match: | 1245 if not match: |
1268 DieWithError('Must pass an issue ID or full URL for ' | 1246 DieWithError('Must pass an issue ID or full URL for ' |
1269 '\'Download raw patch set\'') | 1247 '\'Download raw patch set\'') |
1270 issue = match.group(1) | 1248 issue = match.group(1) |
1271 patch_data = urllib2.urlopen(issue_arg).read() | 1249 patch_data = urllib2.urlopen(issue_arg).read() |
1272 | 1250 |
1273 if options.newbranch: | 1251 if options.newbranch: |
1274 if options.force: | 1252 if options.force: |
1275 RunGit(['branch', '-D', options.newbranch], | 1253 RunGit(['branch', '-D', options.newbranch], |
1276 swallow_stderr=True, error_ok=True) | 1254 stderr=subprocess2.PIPE, error_ok=True) |
1277 RunGit(['checkout', '-b', options.newbranch, | 1255 RunGit(['checkout', '-b', options.newbranch, |
1278 Changelist().GetUpstreamBranch()]) | 1256 Changelist().GetUpstreamBranch()]) |
1279 | 1257 |
1280 # Switch up to the top-level directory, if necessary, in preparation for | 1258 # Switch up to the top-level directory, if necessary, in preparation for |
1281 # applying the patch. | 1259 # applying the patch. |
1282 top = RunGit(['rev-parse', '--show-cdup']).strip() | 1260 top = RunGit(['rev-parse', '--show-cdup']).strip() |
1283 if top: | 1261 if top: |
1284 os.chdir(top) | 1262 os.chdir(top) |
1285 | 1263 |
1286 # Git patches have a/ at the beginning of source paths. We strip that out | 1264 # Git patches have a/ at the beginning of source paths. We strip that out |
1287 # with a sed script rather than the -p flag to patch so we can feed either | 1265 # with a sed script rather than the -p flag to patch so we can feed either |
1288 # Git or svn-style patches into the same apply command. | 1266 # Git or svn-style patches into the same apply command. |
1289 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7. | 1267 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7. |
1290 sed_proc = Popen(['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], | 1268 try: |
1291 stdin=subprocess.PIPE, stdout=subprocess.PIPE) | 1269 patch_data = subprocess2.check_output( |
1292 patch_data = sed_proc.communicate(patch_data)[0] | 1270 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], |
1293 if sed_proc.returncode: | 1271 stdin=patch_data) |
| 1272 except subprocess2.CalledProcessError: |
1294 DieWithError('Git patch mungling failed.') | 1273 DieWithError('Git patch mungling failed.') |
1295 logging.info(patch_data) | 1274 logging.info(patch_data) |
1296 # We use "git apply" to apply the patch instead of "patch" so that we can | 1275 # We use "git apply" to apply the patch instead of "patch" so that we can |
1297 # pick up file adds. | 1276 # pick up file adds. |
1298 # The --index flag means: also insert into the index (so we catch adds). | 1277 # The --index flag means: also insert into the index (so we catch adds). |
1299 cmd = ['git', 'apply', '--index', '-p0'] | 1278 cmd = ['git', 'apply', '--index', '-p0'] |
1300 if options.reject: | 1279 if options.reject: |
1301 cmd.append('--reject') | 1280 cmd.append('--reject') |
1302 patch_proc = Popen(cmd, stdin=subprocess.PIPE) | 1281 try: |
1303 patch_proc.communicate(patch_data) | 1282 subprocess2.check_call(cmd, stdin=patch_data) |
1304 if patch_proc.returncode: | 1283 except subprocess2.CalledProcessError: |
1305 DieWithError('Failed to apply the patch') | 1284 DieWithError('Failed to apply the patch') |
1306 | 1285 |
1307 # If we had an issue, commit the current state and register the issue. | 1286 # If we had an issue, commit the current state and register the issue. |
1308 if not options.nocommit: | 1287 if not options.nocommit: |
1309 RunGit(['commit', '-m', 'patch from issue %s' % issue]) | 1288 RunGit(['commit', '-m', 'patch from issue %s' % issue]) |
1310 cl = Changelist() | 1289 cl = Changelist() |
1311 cl.SetIssue(issue) | 1290 cl.SetIssue(issue) |
1312 print "Committed patch." | 1291 print "Committed patch." |
1313 else: | 1292 else: |
1314 print "Patch applied to index." | 1293 print "Patch applied to index." |
1315 return 0 | 1294 return 0 |
1316 | 1295 |
1317 | 1296 |
1318 def CMDrebase(parser, args): | 1297 def CMDrebase(parser, args): |
1319 """rebase current branch on top of svn repo""" | 1298 """rebase current branch on top of svn repo""" |
1320 # Provide a wrapper for git svn rebase to help avoid accidental | 1299 # Provide a wrapper for git svn rebase to help avoid accidental |
1321 # git svn dcommit. | 1300 # git svn dcommit. |
1322 # It's the only command that doesn't use parser at all since we just defer | 1301 # It's the only command that doesn't use parser at all since we just defer |
1323 # execution to git-svn. | 1302 # execution to git-svn. |
1324 RunGit(['svn', 'rebase'] + args, redirect_stdout=False) | 1303 subprocess2.check_call(['git', 'svn', 'rebase'] + args) |
1325 return 0 | 1304 return 0 |
1326 | 1305 |
1327 | 1306 |
1328 def GetTreeStatus(): | 1307 def GetTreeStatus(): |
1329 """Fetches the tree status and returns either 'open', 'closed', | 1308 """Fetches the tree status and returns either 'open', 'closed', |
1330 'unknown' or 'unset'.""" | 1309 'unknown' or 'unset'.""" |
1331 url = settings.GetTreeStatusUrl(error_ok=True) | 1310 url = settings.GetTreeStatusUrl(error_ok=True) |
1332 if url: | 1311 if url: |
1333 status = urllib2.urlopen(url).read().lower() | 1312 status = urllib2.urlopen(url).read().lower() |
1334 if status.find('closed') != -1 or status == '0': | 1313 if status.find('closed') != -1 or status == '0': |
(...skipping 116 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1451 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e))) | 1430 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e))) |
1452 | 1431 |
1453 # Not a known command. Default to help. | 1432 # Not a known command. Default to help. |
1454 GenUsage(parser, 'help') | 1433 GenUsage(parser, 'help') |
1455 return CMDhelp(parser, argv) | 1434 return CMDhelp(parser, argv) |
1456 | 1435 |
1457 | 1436 |
1458 if __name__ == '__main__': | 1437 if __name__ == '__main__': |
1459 fix_encoding.fix_encoding() | 1438 fix_encoding.fix_encoding() |
1460 sys.exit(main(sys.argv[1:])) | 1439 sys.exit(main(sys.argv[1:])) |
OLD | NEW |