| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # | 2 # |
| 3 # Copyright 2007 Google Inc. | 3 # Copyright 2007 Google Inc. |
| 4 # | 4 # |
| 5 # Licensed under the Apache License, Version 2.0 (the "License"); | 5 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 # you may not use this file except in compliance with the License. | 6 # you may not use this file except in compliance with the License. |
| 7 # You may obtain a copy of the License at | 7 # You may obtain a copy of the License at |
| 8 # | 8 # |
| 9 # http://www.apache.org/licenses/LICENSE-2.0 | 9 # http://www.apache.org/licenses/LICENSE-2.0 |
| 10 # | 10 # |
| 11 # Unless required by applicable law or agreed to in writing, software | 11 # Unless required by applicable law or agreed to in writing, software |
| 12 # distributed under the License is distributed on an "AS IS" BASIS, | 12 # distributed under the License is distributed on an "AS IS" BASIS, |
| 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 # See the License for the specific language governing permissions and | 14 # See the License for the specific language governing permissions and |
| 15 # limitations under the License. | 15 # limitations under the License. |
| 16 | 16 |
| 17 """Tool for uploading diffs from a version control system to the codereview app. | 17 """Tool for uploading diffs from a version control system to the codereview app. |
| 18 | 18 |
| 19 Usage summary: upload.py [options] [-- diff_options] | 19 Usage summary: upload.py [options] [-- diff_options] |
| 20 | 20 |
| 21 Diff options are passed to the diff command of the underlying system. | 21 Diff options are passed to the diff command of the underlying system. |
| 22 | 22 |
| 23 Supported version control systems: | 23 Supported version control systems: |
| 24 Bazaar | |
| 24 Git | 25 Git |
| 25 Mercurial | 26 Mercurial |
| 26 Subversion | 27 Subversion |
| 27 | 28 |
| 28 It is important for Git/Mercurial users to specify a tree/node/branch to diff | 29 It is important for Git/Mercurial users to specify a tree/node/branch to diff |
| 29 against by using the '--rev' option. | 30 against by using the '--rev' option. |
| 30 """ | 31 """ |
| 31 # This code is derived from appcfg.py in the App Engine SDK (open source), | 32 # This code is derived from appcfg.py in the App Engine SDK (open source), |
| 32 # and from ASPN recipe #146306. | 33 # and from ASPN recipe #146306. |
| 33 | 34 |
| (...skipping 534 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 568 p.wait() | 569 p.wait() |
| 569 errout = p.stderr.read() | 570 errout = p.stderr.read() |
| 570 if print_output and errout: | 571 if print_output and errout: |
| 571 print >>sys.stderr, errout | 572 print >>sys.stderr, errout |
| 572 p.stdout.close() | 573 p.stdout.close() |
| 573 p.stderr.close() | 574 p.stderr.close() |
| 574 return output, p.returncode | 575 return output, p.returncode |
| 575 | 576 |
| 576 | 577 |
| 577 def RunShell(command, silent_ok=False, universal_newlines=True, | 578 def RunShell(command, silent_ok=False, universal_newlines=True, |
| 578 print_output=False): | 579 print_output=False,check_returncode=True): |
| 579 data, retcode = RunShellWithReturnCode(command, print_output, | 580 data, retcode = RunShellWithReturnCode(command, print_output, |
| 580 universal_newlines) | 581 universal_newlines) |
| 581 if retcode: | 582 |
| 583 if retcode and check_returncode: | |
| 582 ErrorExit("Got error status from %s:\n%s" % (command, data)) | 584 ErrorExit("Got error status from %s:\n%s" % (command, data)) |
| 583 if not silent_ok and not data: | 585 if not silent_ok and not data: |
| 584 ErrorExit("No output from %s" % command) | 586 ErrorExit("No output from %s" % command) |
| 585 return data | 587 return data |
| 586 | 588 |
| 587 | 589 |
| 588 class VersionControlSystem(object): | 590 class VersionControlSystem(object): |
| 589 """Abstract base class providing an interface to the VCS.""" | 591 """Abstract base class providing an interface to the VCS.""" |
| 590 | 592 |
| 591 def __init__(self, options): | 593 def __init__(self, options): |
| (...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 634 is_binary: True iff the file is binary. | 636 is_binary: True iff the file is binary. |
| 635 status: The status of the file. | 637 status: The status of the file. |
| 636 """ | 638 """ |
| 637 | 639 |
| 638 raise NotImplementedError( | 640 raise NotImplementedError( |
| 639 "abstract method -- subclass %s must override" % self.__class__) | 641 "abstract method -- subclass %s must override" % self.__class__) |
| 640 | 642 |
| 641 | 643 |
| 642 def GetBaseFiles(self, diff): | 644 def GetBaseFiles(self, diff): |
| 643 """Helper that calls GetBase file for each file in the patch. | 645 """Helper that calls GetBase file for each file in the patch. |
| 644 | 646 |
| 645 Returns: | 647 Returns: |
| 646 A dictionary that maps from filename to GetBaseFile's tuple. Filenames | 648 A dictionary that maps from filename to GetBaseFile's tuple. Filenames |
| 647 are retrieved based on lines that start with "Index:" or | 649 are retrieved based on lines that start with "Index:" or |
| 648 "Property changes on:". | 650 "Property changes on:". |
| 649 """ | 651 """ |
| 650 files = {} | 652 files = {} |
| 651 for line in diff.splitlines(True): | 653 for line in diff.splitlines(True): |
| 652 if line.startswith('Index:') or line.startswith('Property changes on:'): | 654 if line.startswith('Index:') or line.startswith('Property changes on:'): |
| 653 unused, filename = line.split(':', 1) | 655 unused, filename = line.split(':', 1) |
| 654 # On Windows if a file has property changes its filename uses '\' | 656 # On Windows if a file has property changes its filename uses '\' |
| (...skipping 453 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1108 is_binary = False | 1110 is_binary = False |
| 1109 oldrelpath = relpath = self._GetRelPath(filename) | 1111 oldrelpath = relpath = self._GetRelPath(filename) |
| 1110 # "hg status -C" returns two lines for moved/copied files, one otherwise | 1112 # "hg status -C" returns two lines for moved/copied files, one otherwise |
| 1111 out = RunShell(["hg", "status", "-C", "--rev", self.base_rev, relpath]) | 1113 out = RunShell(["hg", "status", "-C", "--rev", self.base_rev, relpath]) |
| 1112 out = out.splitlines() | 1114 out = out.splitlines() |
| 1113 # HACK: strip error message about missing file/directory if it isn't in | 1115 # HACK: strip error message about missing file/directory if it isn't in |
| 1114 # the working copy | 1116 # the working copy |
| 1115 if out[0].startswith('%s: ' % relpath): | 1117 if out[0].startswith('%s: ' % relpath): |
| 1116 out = out[1:] | 1118 out = out[1:] |
| 1117 if len(out) > 1: | 1119 if len(out) > 1: |
| 1118 # Moved/copied => considered as modified, use old filename to | 1120 # Moved/copied => considered as modified, use old filename to |
| 1119 # retrieve base contents | 1121 # retrieve base contents |
| 1120 oldrelpath = out[1].strip() | 1122 oldrelpath = out[1].strip() |
| 1121 status = "M" | 1123 status = "M" |
| 1122 else: | 1124 else: |
| 1123 status, _ = out[0].split(' ', 1) | 1125 status, _ = out[0].split(' ', 1) |
| 1124 if status != "A": | 1126 if status != "A": |
| 1125 base_content = RunShell(["hg", "cat", "-r", self.base_rev, oldrelpath], | 1127 base_content = RunShell(["hg", "cat", "-r", self.base_rev, oldrelpath], |
| 1126 silent_ok=True) | 1128 silent_ok=True) |
| 1127 is_binary = "\0" in base_content # Mercurial's heuristic | 1129 is_binary = "\0" in base_content # Mercurial's heuristic |
| 1128 if status != "R": | 1130 if status != "R": |
| 1129 new_content = open(relpath, "rb").read() | 1131 new_content = open(relpath, "rb").read() |
| 1130 is_binary = is_binary or "\0" in new_content | 1132 is_binary = is_binary or "\0" in new_content |
| 1131 if is_binary and base_content: | 1133 if is_binary and base_content: |
| 1132 # Fetch again without converting newlines | 1134 # Fetch again without converting newlines |
| 1133 base_content = RunShell(["hg", "cat", "-r", self.base_rev, oldrelpath], | 1135 base_content = RunShell(["hg", "cat", "-r", self.base_rev, oldrelpath], |
| 1134 silent_ok=True, universal_newlines=False) | 1136 silent_ok=True, universal_newlines=False) |
| 1135 if not is_binary or not self.IsImage(relpath): | 1137 if not is_binary or not self.IsImage(relpath): |
| 1136 new_content = None | 1138 new_content = None |
| 1137 return base_content, new_content, is_binary, status | 1139 return base_content, new_content, is_binary, status |
| 1140 | |
| 1141 | |
| 1142 class BazaarVCS(VersionControlSystem): | |
| 1143 """Implementation of the VersionControlSystem interface for Bazaar.""" | |
| 1144 | |
| 1145 def GenerateDiff(self, args): | |
| 1146 """Return the current diff as a string. | |
| 1147 | |
| 1148 Args: | |
| 1149 args: Extra arguments to pass to the diff command. | |
| 1150 """ | |
| 1151 if self.options.revision: | |
| 1152 args = ["-r", self.options.revision] + args | |
| 1153 # We need check_returncode = False because bzr diff returns 1 if changes | |
| 1154 # are found. | |
| 1155 | |
| 1156 data,retcode = RunShellWithReturnCode(["bzr", "diff"] + args, True) | |
| 1157 filecount = 0 | |
| 1158 lines = data.splitlines() | |
| 1159 for i, line in enumerate(lines): | |
| 1160 match = re.match(r"^=== (added|removed|modified) file '(.*)'$", line) | |
| 1161 if match: | |
| 1162 # Modify line to make it look like as it comes from svn diff. | |
| 1163 lines[i] = "Index: %s" % match.group(2) | |
| 1164 filecount += 1 | |
| 1165 logging.info(line) | |
| 1166 if not filecount: | |
| 1167 ErrorExit("No valid patches found in output from bzr diff") | |
| 1168 return "\n".join(lines) | |
| 1169 | |
| 1170 def GetUnknownFiles(self): | |
| 1171 """Return a list of files unknown to the VCS.""" | |
| 1172 args = ["-S"] | |
| 1173 if self.options.revision: | |
| 1174 args += ["-r", self.options.revision] | |
| 1175 status = RunShell(["bzr", "status"] + args, silent_ok=True) | |
| 1176 unknown_files = [] | |
| 1177 for line in status.splitlines(): | |
| 1178 if line and line[0] == "?": | |
| 1179 unknown_files.append(line) | |
| 1180 return unknown_files | |
| 1181 | |
| 1182 def GetBaseFile(self, filename): | |
| 1183 """Get the content of the upstream version of a file. | |
| 1184 | |
| 1185 Returns: | |
| 1186 A tuple (content, status) representing the file content and the status of | |
| 1187 the file. | |
| 1188 """ | |
| 1189 st_args = [] | |
| 1190 cat_args = [] | |
| 1191 new_content = None | |
| 1192 if self.options.revision: | |
| 1193 if '..' in self.options.revision: | |
| 1194 start_rev = self.options.revision.split('..', 1)[0] | |
| 1195 cat_args += ["-r", start_rev] | |
| 1196 else: | |
| 1197 cat_args += ["-r", self.options.revision] | |
| 1198 st_args += ["-r", self.options.revision] | |
| 1199 status = RunShell(["bzr", "status", "-S"] + st_args + [filename]) | |
| 1200 if status[1] == "N": | |
| 1201 content = "" | |
| 1202 else: | |
| 1203 content = RunShell(["bzr", "cat"] + cat_args + [filename]) | |
| 1204 return content, new_content, False, status[0:4] | |
| 1138 | 1205 |
| 1139 | 1206 |
| 1140 # NOTE: The SplitPatch function is duplicated in engine.py, keep them in sync. | 1207 # NOTE: The SplitPatch function is duplicated in engine.py, keep them in sync. |
| 1141 def SplitPatch(data): | 1208 def SplitPatch(data): |
| 1142 """Splits a patch into separate pieces for each file. | 1209 """Splits a patch into separate pieces for each file. |
| 1143 | 1210 |
| 1144 Args: | 1211 Args: |
| 1145 data: A string containing the output of svn diff. | 1212 data: A string containing the output of svn diff. |
| 1146 | 1213 |
| 1147 Returns: | 1214 Returns: |
| (...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1219 # Mercurial has a command to get the base directory of a repository | 1286 # Mercurial has a command to get the base directory of a repository |
| 1220 # Try running it, but don't die if we don't have hg installed. | 1287 # Try running it, but don't die if we don't have hg installed. |
| 1221 # NOTE: we try Mercurial first as it can sit on top of an SVN working copy. | 1288 # NOTE: we try Mercurial first as it can sit on top of an SVN working copy. |
| 1222 try: | 1289 try: |
| 1223 out, returncode = RunShellWithReturnCode(["hg", "root"]) | 1290 out, returncode = RunShellWithReturnCode(["hg", "root"]) |
| 1224 if returncode == 0: | 1291 if returncode == 0: |
| 1225 return MercurialVCS(options, out.strip()) | 1292 return MercurialVCS(options, out.strip()) |
| 1226 except OSError, (errno, message): | 1293 except OSError, (errno, message): |
| 1227 if errno != 2: # ENOENT -- they don't have hg installed. | 1294 if errno != 2: # ENOENT -- they don't have hg installed. |
| 1228 raise | 1295 raise |
| 1229 | 1296 try: |
|
bialix
2009/03/24 22:49:23
I think you need either use command ["bzr", "--no-
| |
| 1297 out, returncode = RunShellWithReturnCode(["bzr", "root"]) | |
| 1298 if returncode == 0: | |
| 1299 os.chdir(out.strip()) | |
| 1300 return BazaarVCS(options) | |
| 1301 except OSError, (errno, message): | |
| 1302 if errno != 2: # ENOENT -- they don't have bzr installed. | |
| 1303 raise | |
| 1230 # Subversion has a .svn in all working directories. | 1304 # Subversion has a .svn in all working directories. |
| 1231 if os.path.isdir('.svn'): | 1305 if os.path.isdir('.svn'): |
| 1232 logging.info("Guessed VCS = Subversion") | 1306 logging.info("Guessed VCS = Subversion") |
| 1233 return SubversionVCS(options) | 1307 return SubversionVCS(options) |
| 1234 | 1308 |
| 1235 # Git has a command to test if you're in a git tree. | 1309 # Git has a command to test if you're in a git tree. |
| 1236 # Try running it, but don't die if we don't have git installed. | 1310 # Try running it, but don't die if we don't have git installed. |
| 1237 try: | 1311 try: |
| 1238 out, returncode = RunShellWithReturnCode(["git", "rev-parse", | 1312 out, returncode = RunShellWithReturnCode(["git", "rev-parse", |
| 1239 "--is-inside-work-tree"]) | 1313 "--is-inside-work-tree"]) |
| 1240 if returncode == 0: | 1314 if returncode == 0: |
| 1241 return GitVCS(options) | 1315 return GitVCS(options) |
| 1242 except OSError, (errno, message): | 1316 except OSError, (errno, message): |
| 1243 if errno != 2: # ENOENT -- they don't have git installed. | 1317 if errno != 2: # ENOENT -- they don't have git installed. |
| 1244 raise | 1318 raise |
| 1245 | 1319 |
| 1246 ErrorExit(("Could not guess version control system. " | 1320 ErrorExit(("Could not guess version control system. " |
| 1247 "Are you in a working copy directory?")) | 1321 "Are you in a working copy directory?")) |
| 1248 | 1322 |
| 1249 | 1323 |
| 1250 def RealMain(argv, data=None): | 1324 def RealMain(argv, data=None): |
| 1251 """The real main function. | |
| 1252 | |
| 1253 Args: | |
| 1254 argv: Command line arguments. | |
| 1255 data: Diff contents. If None (default) the diff is generated by | |
| 1256 the VersionControlSystem implementation returned by GuessVCS(). | |
| 1257 | |
| 1258 Returns: | |
| 1259 A 2-tuple (issue id, patchset id). | |
| 1260 The patchset id is None if the base files are not uploaded by this | |
| 1261 script (applies only to SVN checkouts). | |
| 1262 """ | |
| 1263 logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:" | 1325 logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:" |
| 1264 "%(lineno)s %(message)s ")) | 1326 "%(lineno)s %(message)s ")) |
| 1265 os.environ['LC_ALL'] = 'C' | 1327 os.environ['LC_ALL'] = 'C' |
| 1266 options, args = parser.parse_args(argv[1:]) | 1328 options, args = parser.parse_args(argv[1:]) |
| 1267 global verbosity | 1329 global verbosity |
| 1268 verbosity = options.verbose | 1330 verbosity = options.verbose |
| 1269 if verbosity >= 3: | 1331 if verbosity >= 3: |
| 1270 logging.getLogger().setLevel(logging.DEBUG) | 1332 logging.getLogger().setLevel(logging.DEBUG) |
| 1271 elif verbosity >= 2: | 1333 elif verbosity >= 2: |
| 1272 logging.getLogger().setLevel(logging.INFO) | 1334 logging.getLogger().setLevel(logging.INFO) |
| (...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1338 if not options.download_base: | 1400 if not options.download_base: |
| 1339 form_fields.append(("content_upload", "1")) | 1401 form_fields.append(("content_upload", "1")) |
| 1340 if len(data) > MAX_UPLOAD_SIZE: | 1402 if len(data) > MAX_UPLOAD_SIZE: |
| 1341 print "Patch is large, so uploading file patches separately." | 1403 print "Patch is large, so uploading file patches separately." |
| 1342 uploaded_diff_file = [] | 1404 uploaded_diff_file = [] |
| 1343 form_fields.append(("separate_patches", "1")) | 1405 form_fields.append(("separate_patches", "1")) |
| 1344 else: | 1406 else: |
| 1345 uploaded_diff_file = [("data", "data.diff", data)] | 1407 uploaded_diff_file = [("data", "data.diff", data)] |
| 1346 ctype, body = EncodeMultipartFormData(form_fields, uploaded_diff_file) | 1408 ctype, body = EncodeMultipartFormData(form_fields, uploaded_diff_file) |
| 1347 response_body = rpc_server.Send("/upload", body, content_type=ctype) | 1409 response_body = rpc_server.Send("/upload", body, content_type=ctype) |
| 1348 patchset = None | |
| 1349 if not options.download_base or not uploaded_diff_file: | 1410 if not options.download_base or not uploaded_diff_file: |
| 1350 lines = response_body.splitlines() | 1411 lines = response_body.splitlines() |
| 1351 if len(lines) >= 2: | 1412 if len(lines) >= 2: |
| 1352 msg = lines[0] | 1413 msg = lines[0] |
| 1353 patchset = lines[1].strip() | 1414 patchset = lines[1].strip() |
| 1354 patches = [x.split(" ", 1) for x in lines[2:]] | 1415 patches = [x.split(" ", 1) for x in lines[2:]] |
| 1355 else: | 1416 else: |
| 1356 msg = response_body | 1417 msg = response_body |
| 1357 else: | 1418 else: |
| 1358 msg = response_body | 1419 msg = response_body |
| (...skipping 19 matching lines...) Expand all Loading... | |
| 1378 try: | 1439 try: |
| 1379 RealMain(sys.argv) | 1440 RealMain(sys.argv) |
| 1380 except KeyboardInterrupt: | 1441 except KeyboardInterrupt: |
| 1381 print | 1442 print |
| 1382 StatusUpdate("Interrupted.") | 1443 StatusUpdate("Interrupted.") |
| 1383 sys.exit(1) | 1444 sys.exit(1) |
| 1384 | 1445 |
| 1385 | 1446 |
| 1386 if __name__ == "__main__": | 1447 if __name__ == "__main__": |
| 1387 main() | 1448 main() |
| OLD | NEW |