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] [path...] |
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 Git | 24 Git |
25 Mercurial | 25 Mercurial |
26 Subversion | 26 Subversion |
27 | 27 |
28 It is important for Git/Mercurial users to specify a tree/node/branch to diff | 28 It is important for Git/Mercurial users to specify a tree/node/branch to diff |
29 against by using the '--rev' option. | 29 against by using the '--rev' option. |
(...skipping 410 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
440 os.close(fd) | 440 os.close(fd) |
441 # Always chmod the cookie file | 441 # Always chmod the cookie file |
442 os.chmod(self.cookie_file, 0600) | 442 os.chmod(self.cookie_file, 0600) |
443 else: | 443 else: |
444 # Don't save cookies across runs of update.py. | 444 # Don't save cookies across runs of update.py. |
445 self.cookie_jar = cookielib.CookieJar() | 445 self.cookie_jar = cookielib.CookieJar() |
446 opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar)) | 446 opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar)) |
447 return opener | 447 return opener |
448 | 448 |
449 | 449 |
450 parser = optparse.OptionParser(usage="%prog [options] [-- diff_options]") | 450 parser = optparse.OptionParser( |
| 451 usage="%prog [options] [-- diff_options] [path...]") |
451 parser.add_option("-y", "--assume_yes", action="store_true", | 452 parser.add_option("-y", "--assume_yes", action="store_true", |
452 dest="assume_yes", default=False, | 453 dest="assume_yes", default=False, |
453 help="Assume that the answer to yes/no questions is 'yes'.") | 454 help="Assume that the answer to yes/no questions is 'yes'.") |
454 # Logging | 455 # Logging |
455 group = parser.add_option_group("Logging options") | 456 group = parser.add_option_group("Logging options") |
456 group.add_option("-q", "--quiet", action="store_const", const=0, | 457 group.add_option("-q", "--quiet", action="store_const", const=0, |
457 dest="verbose", help="Print errors only.") | 458 dest="verbose", help="Print errors only.") |
458 group.add_option("-v", "--verbose", action="store_const", const=2, | 459 group.add_option("-v", "--verbose", action="store_const", const=2, |
459 dest="verbose", default=1, | 460 dest="verbose", default=1, |
460 help="Print info level logs (default).") | 461 help="Print info level logs.") |
461 group.add_option("--noisy", action="store_const", const=3, | 462 group.add_option("--noisy", action="store_const", const=3, |
462 dest="verbose", help="Print all logs.") | 463 dest="verbose", help="Print all logs.") |
463 # Review server | 464 # Review server |
464 group = parser.add_option_group("Review server options") | 465 group = parser.add_option_group("Review server options") |
465 group.add_option("-s", "--server", action="store", dest="server", | 466 group.add_option("-s", "--server", action="store", dest="server", |
466 default=DEFAULT_REVIEW_SERVER, | 467 default=DEFAULT_REVIEW_SERVER, |
467 metavar="SERVER", | 468 metavar="SERVER", |
468 help=("The server to upload to. The format is host[:port]. " | 469 help=("The server to upload to. The format is host[:port]. " |
469 "Defaults to '%default'.")) | 470 "Defaults to '%default'.")) |
470 group.add_option("-e", "--email", action="store", dest="email", | 471 group.add_option("-e", "--email", action="store", dest="email", |
(...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
548 or 'HOSTED'. Defaults to AUTH_ACCOUNT_TYPE. | 549 or 'HOSTED'. Defaults to AUTH_ACCOUNT_TYPE. |
549 | 550 |
550 Returns: | 551 Returns: |
551 A new AbstractRpcServer, on which RPC calls can be made. | 552 A new AbstractRpcServer, on which RPC calls can be made. |
552 """ | 553 """ |
553 | 554 |
554 rpc_server_class = HttpRpcServer | 555 rpc_server_class = HttpRpcServer |
555 | 556 |
556 # If this is the dev_appserver, use fake authentication. | 557 # If this is the dev_appserver, use fake authentication. |
557 host = (host_override or server).lower() | 558 host = (host_override or server).lower() |
558 if host == "localhost" or host.startswith("localhost:"): | 559 if re.match(r'(http://)?localhost([:/]|$)', host): |
559 if email is None: | 560 if email is None: |
560 email = "test@example.com" | 561 email = "test@example.com" |
561 logging.info("Using debug user %s. Override with --email" % email) | 562 logging.info("Using debug user %s. Override with --email" % email) |
562 server = rpc_server_class( | 563 server = rpc_server_class( |
563 server, | 564 server, |
564 lambda: (email, "password"), | 565 lambda: (email, "password"), |
565 host_override=host_override, | 566 host_override=host_override, |
566 extra_headers={"Cookie": | 567 extra_headers={"Cookie": |
567 'dev_appserver_login="%s:False"' % email}, | 568 'dev_appserver_login="%s:False"' % email}, |
568 save_cookies=save_cookies, | 569 save_cookies=save_cookies, |
(...skipping 291 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
860 # Base URL is required to fetch files deleted in an older revision. | 861 # Base URL is required to fetch files deleted in an older revision. |
861 # Result is cached to not guess it over and over again in GetBaseFile(). | 862 # Result is cached to not guess it over and over again in GetBaseFile(). |
862 required = self.options.download_base or self.options.revision is not None | 863 required = self.options.download_base or self.options.revision is not None |
863 self.svn_base = self._GuessBase(required) | 864 self.svn_base = self._GuessBase(required) |
864 | 865 |
865 def GuessBase(self, required): | 866 def GuessBase(self, required): |
866 """Wrapper for _GuessBase.""" | 867 """Wrapper for _GuessBase.""" |
867 return self.svn_base | 868 return self.svn_base |
868 | 869 |
869 def _GuessBase(self, required): | 870 def _GuessBase(self, required): |
870 """Returns the SVN base URL. | 871 """Returns base URL for current diff. |
871 | 872 |
872 Args: | 873 Args: |
873 required: If true, exits if the url can't be guessed, otherwise None is | 874 required: If true, exits if the url can't be guessed, otherwise None is |
874 returned. | 875 returned. |
875 """ | 876 """ |
876 info = RunShell(["svn", "info"]) | 877 info = RunShell(["svn", "info"]) |
877 for line in info.splitlines(): | 878 for line in info.splitlines(): |
878 words = line.split() | 879 if line.startswith("URL: "): |
879 if len(words) == 2 and words[0] == "URL:": | 880 url = line.split()[1] |
880 url = words[1] | |
881 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url) | 881 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url) |
882 username, netloc = urllib.splituser(netloc) | 882 username, netloc = urllib.splituser(netloc) |
883 if username: | 883 if username: |
884 logging.info("Removed username from base URL") | 884 logging.info("Removed username from base URL") |
885 if netloc.endswith("svn.python.org"): | 885 guess = "" |
886 if netloc == "svn.python.org": | 886 if netloc == "svn.python.org" and scheme == "svn+ssh": |
887 if path.startswith("/projects/"): | 887 path = "projects" + path |
888 path = path[9:] | 888 scheme = "http" |
889 elif netloc != "pythondev@svn.python.org": | 889 guess = "Python " |
890 ErrorExit("Unrecognized Python URL: %s" % url) | |
891 base = "http://svn.python.org/view/*checkout*%s/" % path | |
892 logging.info("Guessed Python base = %s", base) | |
893 elif netloc.endswith("svn.collab.net"): | |
894 if path.startswith("/repos/"): | |
895 path = path[6:] | |
896 base = "http://svn.collab.net/viewvc/*checkout*%s/" % path | |
897 logging.info("Guessed CollabNet base = %s", base) | |
898 elif netloc.endswith(".googlecode.com"): | 890 elif netloc.endswith(".googlecode.com"): |
899 path = path + "/" | 891 scheme = "http" |
900 base = urlparse.urlunparse(("http", netloc, path, params, | 892 guess = "Google Code " |
901 query, fragment)) | 893 path = path + "/" |
902 logging.info("Guessed Google Code base = %s", base) | 894 base = urlparse.urlunparse((scheme, netloc, path, params, |
903 else: | 895 query, fragment)) |
904 path = path + "/" | 896 logging.info("Guessed %sbase = %s", guess, base) |
905 base = urlparse.urlunparse((scheme, netloc, path, params, | |
906 query, fragment)) | |
907 logging.info("Guessed base = %s", base) | |
908 return base | 897 return base |
909 if required: | 898 if required: |
910 ErrorExit("Can't find URL in output from svn info") | 899 ErrorExit("Can't find URL in output from svn info") |
911 return None | 900 return None |
912 | 901 |
913 def GenerateDiff(self, args): | 902 def GenerateDiff(self, args): |
914 cmd = ["svn", "diff"] | 903 cmd = ["svn", "diff"] |
915 if self.options.revision: | 904 if self.options.revision: |
916 cmd += ["-r", self.options.revision] | 905 cmd += ["-r", self.options.revision] |
917 cmd.extend(args) | 906 cmd.extend(args) |
(...skipping 260 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1178 if not filecount: | 1167 if not filecount: |
1179 ErrorExit("No valid patches found in output from git diff") | 1168 ErrorExit("No valid patches found in output from git diff") |
1180 # Add auto property for the last seen file. | 1169 # Add auto property for the last seen file. |
1181 assert filename is not None | 1170 assert filename is not None |
1182 AddSubversionPropertyChange(filename) | 1171 AddSubversionPropertyChange(filename) |
1183 return "".join(svndiff) | 1172 return "".join(svndiff) |
1184 | 1173 |
1185 def GenerateDiff(self, extra_args): | 1174 def GenerateDiff(self, extra_args): |
1186 extra_args = extra_args[:] | 1175 extra_args = extra_args[:] |
1187 if self.options.revision: | 1176 if self.options.revision: |
1188 extra_args = [self.options.revision] + extra_args | 1177 if ":" in self.options.revision: |
| 1178 extra_args = self.options.revision.split(":", 1) + extra_args |
| 1179 else: |
| 1180 extra_args = [self.options.revision] + extra_args |
1189 | 1181 |
1190 # --no-ext-diff is broken in some versions of Git, so try to work around | 1182 # --no-ext-diff is broken in some versions of Git, so try to work around |
1191 # this by overriding the environment (but there is still a problem if the | 1183 # this by overriding the environment (but there is still a problem if the |
1192 # git config key "diff.external" is used). | 1184 # git config key "diff.external" is used). |
1193 env = os.environ.copy() | 1185 env = os.environ.copy() |
1194 if 'GIT_EXTERNAL_DIFF' in env: del env['GIT_EXTERNAL_DIFF'] | 1186 if 'GIT_EXTERNAL_DIFF' in env: del env['GIT_EXTERNAL_DIFF'] |
1195 return RunShell(["git", "diff", "--no-ext-diff", "--full-index", "-M"] | 1187 return RunShell(["git", "diff", "--no-ext-diff", "--full-index", "-M"] |
1196 + extra_args, env=env) | 1188 + extra_args, env=env) |
1197 | 1189 |
1198 def GetUnknownFiles(self): | 1190 def GetUnknownFiles(self): |
(...skipping 61 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1260 else: | 1252 else: |
1261 self.base_rev = RunShell(["hg", "parent", "-q"]).split(':')[1].strip() | 1253 self.base_rev = RunShell(["hg", "parent", "-q"]).split(':')[1].strip() |
1262 | 1254 |
1263 def _GetRelPath(self, filename): | 1255 def _GetRelPath(self, filename): |
1264 """Get relative path of a file according to the current directory, | 1256 """Get relative path of a file according to the current directory, |
1265 given its logical path in the repo.""" | 1257 given its logical path in the repo.""" |
1266 assert filename.startswith(self.subdir), (filename, self.subdir) | 1258 assert filename.startswith(self.subdir), (filename, self.subdir) |
1267 return filename[len(self.subdir):].lstrip(r"\/") | 1259 return filename[len(self.subdir):].lstrip(r"\/") |
1268 | 1260 |
1269 def GenerateDiff(self, extra_args): | 1261 def GenerateDiff(self, extra_args): |
1270 # If no file specified, restrict to the current subdir | |
1271 extra_args = extra_args or ["."] | |
1272 cmd = ["hg", "diff", "--git", "-r", self.base_rev] + extra_args | 1262 cmd = ["hg", "diff", "--git", "-r", self.base_rev] + extra_args |
1273 data = RunShell(cmd, silent_ok=True) | 1263 data = RunShell(cmd, silent_ok=True) |
1274 svndiff = [] | 1264 svndiff = [] |
1275 filecount = 0 | 1265 filecount = 0 |
1276 for line in data.splitlines(): | 1266 for line in data.splitlines(): |
1277 m = re.match("diff --git a/(\S+) b/(\S+)", line) | 1267 m = re.match("diff --git a/(\S+) b/(\S+)", line) |
1278 if m: | 1268 if m: |
1279 # Modify line to make it look like as it comes from svn diff. | 1269 # Modify line to make it look like as it comes from svn diff. |
1280 # With this modification no changes on the server side are required | 1270 # With this modification no changes on the server side are required |
1281 # to make upload.py work with Mercurial repos. | 1271 # to make upload.py work with Mercurial repos. |
(...skipping 30 matching lines...) Expand all Loading... |
1312 new_content = None | 1302 new_content = None |
1313 is_binary = False | 1303 is_binary = False |
1314 oldrelpath = relpath = self._GetRelPath(filename) | 1304 oldrelpath = relpath = self._GetRelPath(filename) |
1315 # "hg status -C" returns two lines for moved/copied files, one otherwise | 1305 # "hg status -C" returns two lines for moved/copied files, one otherwise |
1316 out = RunShell(["hg", "status", "-C", "--rev", self.base_rev, relpath]) | 1306 out = RunShell(["hg", "status", "-C", "--rev", self.base_rev, relpath]) |
1317 out = out.splitlines() | 1307 out = out.splitlines() |
1318 # HACK: strip error message about missing file/directory if it isn't in | 1308 # HACK: strip error message about missing file/directory if it isn't in |
1319 # the working copy | 1309 # the working copy |
1320 if out[0].startswith('%s: ' % relpath): | 1310 if out[0].startswith('%s: ' % relpath): |
1321 out = out[1:] | 1311 out = out[1:] |
1322 if len(out) > 1: | 1312 status, _ = out[0].split(' ', 1) |
| 1313 if len(out) > 1 and status == "A": |
1323 # Moved/copied => considered as modified, use old filename to | 1314 # Moved/copied => considered as modified, use old filename to |
1324 # retrieve base contents | 1315 # retrieve base contents |
1325 oldrelpath = out[1].strip() | 1316 oldrelpath = out[1].strip() |
1326 status = "M" | 1317 status = "M" |
1327 else: | |
1328 status, _ = out[0].split(' ', 1) | |
1329 if ":" in self.base_rev: | 1318 if ":" in self.base_rev: |
1330 base_rev = self.base_rev.split(":", 1)[0] | 1319 base_rev = self.base_rev.split(":", 1)[0] |
1331 else: | 1320 else: |
1332 base_rev = self.base_rev | 1321 base_rev = self.base_rev |
1333 if status != "A": | 1322 if status != "A": |
1334 base_content = RunShell(["hg", "cat", "-r", base_rev, oldrelpath], | 1323 base_content = RunShell(["hg", "cat", "-r", base_rev, oldrelpath], |
1335 silent_ok=True) | 1324 silent_ok=True) |
1336 is_binary = "\0" in base_content # Mercurial's heuristic | 1325 is_binary = "\0" in base_content # Mercurial's heuristic |
1337 if status != "R": | 1326 if status != "R": |
1338 new_content = open(relpath, "rb").read() | 1327 new_content = open(relpath, "rb").read() |
(...skipping 176 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1515 """Returns the content of [auto-props] section of Subversion's config file as | 1504 """Returns the content of [auto-props] section of Subversion's config file as |
1516 a dictionary. | 1505 a dictionary. |
1517 | 1506 |
1518 Returns: | 1507 Returns: |
1519 A dictionary whose key-value pair corresponds the [auto-props] section's | 1508 A dictionary whose key-value pair corresponds the [auto-props] section's |
1520 key-value pair. | 1509 key-value pair. |
1521 In following cases, returns empty dictionary: | 1510 In following cases, returns empty dictionary: |
1522 - config file doesn't exist, or | 1511 - config file doesn't exist, or |
1523 - 'enable-auto-props' is not set to 'true-like-value' in [miscellany]. | 1512 - 'enable-auto-props' is not set to 'true-like-value' in [miscellany]. |
1524 """ | 1513 """ |
1525 # Todo(hayato): Windows users might use different path for configuration file. | 1514 if os.name == 'nt': |
1526 subversion_config = os.path.expanduser("~/.subversion/config") | 1515 subversion_config = os.environ.get("APPDATA") + "\\Subversion\\config" |
| 1516 else: |
| 1517 subversion_config = os.path.expanduser("~/.subversion/config") |
1527 if not os.path.exists(subversion_config): | 1518 if not os.path.exists(subversion_config): |
1528 return {} | 1519 return {} |
1529 config = ConfigParser.ConfigParser() | 1520 config = ConfigParser.ConfigParser() |
1530 config.read(subversion_config) | 1521 config.read(subversion_config) |
1531 if (config.has_section("miscellany") and | 1522 if (config.has_section("miscellany") and |
1532 config.has_option("miscellany", "enable-auto-props") and | 1523 config.has_option("miscellany", "enable-auto-props") and |
1533 config.getboolean("miscellany", "enable-auto-props") and | 1524 config.getboolean("miscellany", "enable-auto-props") and |
1534 config.has_section("auto-props")): | 1525 config.has_section("auto-props")): |
1535 props = {} | 1526 props = {} |
1536 for file_pattern in config.options("auto-props"): | 1527 for file_pattern in config.options("auto-props"): |
(...skipping 228 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1765 try: | 1756 try: |
1766 RealMain(sys.argv) | 1757 RealMain(sys.argv) |
1767 except KeyboardInterrupt: | 1758 except KeyboardInterrupt: |
1768 print | 1759 print |
1769 StatusUpdate("Interrupted.") | 1760 StatusUpdate("Interrupted.") |
1770 sys.exit(1) | 1761 sys.exit(1) |
1771 | 1762 |
1772 | 1763 |
1773 if __name__ == "__main__": | 1764 if __name__ == "__main__": |
1774 main() | 1765 main() |
OLD | NEW |