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