| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # coding: utf-8 |
| 2 # | 3 # |
| 3 # Copyright 2007 Google Inc. | 4 # Copyright 2007 Google Inc. |
| 4 # | 5 # |
| 5 # Licensed under the Apache License, Version 2.0 (the "License"); | 6 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 # you may not use this file except in compliance with the License. | 7 # you may not use this file except in compliance with the License. |
| 7 # You may obtain a copy of the License at | 8 # You may obtain a copy of the License at |
| 8 # | 9 # |
| 9 # http://www.apache.org/licenses/LICENSE-2.0 | 10 # http://www.apache.org/licenses/LICENSE-2.0 |
| 10 # | 11 # |
| 11 # Unless required by applicable law or agreed to in writing, software | 12 # Unless required by applicable law or agreed to in writing, software |
| (...skipping 196 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 208 """Returns an OpenerDirector for making HTTP requests. | 209 """Returns an OpenerDirector for making HTTP requests. |
| 209 | 210 |
| 210 Returns: | 211 Returns: |
| 211 A urllib2.OpenerDirector object. | 212 A urllib2.OpenerDirector object. |
| 212 """ | 213 """ |
| 213 raise NotImplementedError() | 214 raise NotImplementedError() |
| 214 | 215 |
| 215 def _CreateRequest(self, url, data=None): | 216 def _CreateRequest(self, url, data=None): |
| 216 """Creates a new urllib request.""" | 217 """Creates a new urllib request.""" |
| 217 logging.debug("Creating request for: '%s' with payload:\n%s", url, data) | 218 logging.debug("Creating request for: '%s' with payload:\n%s", url, data) |
| 218 req = urllib2.Request(url, data=data) | 219 req = urllib2.Request(url, data=data, headers={"Accept": "text/plain"}) |
| 219 if self.host_override: | 220 if self.host_override: |
| 220 req.add_header("Host", self.host_override) | 221 req.add_header("Host", self.host_override) |
| 221 for key, value in self.extra_headers.iteritems(): | 222 for key, value in self.extra_headers.iteritems(): |
| 222 req.add_header(key, value) | 223 req.add_header(key, value) |
| 223 return req | 224 return req |
| 224 | 225 |
| 225 def _GetAuthToken(self, email, password): | 226 def _GetAuthToken(self, email, password): |
| 226 """Uses ClientLogin to authenticate the user, returning an auth token. | 227 """Uses ClientLogin to authenticate the user, returning an auth token. |
| 227 | 228 |
| 228 Args: | 229 Args: |
| (...skipping 163 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 392 try: | 393 try: |
| 393 f = self.opener.open(req) | 394 f = self.opener.open(req) |
| 394 response = f.read() | 395 response = f.read() |
| 395 f.close() | 396 f.close() |
| 396 return response | 397 return response |
| 397 except urllib2.HTTPError, e: | 398 except urllib2.HTTPError, e: |
| 398 if tries > 3: | 399 if tries > 3: |
| 399 raise | 400 raise |
| 400 elif e.code == 401 or e.code == 302: | 401 elif e.code == 401 or e.code == 302: |
| 401 self._Authenticate() | 402 self._Authenticate() |
| 402 ## elif e.code >= 500 and e.code < 600: | |
| 403 ## # Server Error - try again. | |
| 404 ## continue | |
| 405 elif e.code == 301: | 403 elif e.code == 301: |
| 406 # Handle permanent redirect manually. | 404 # Handle permanent redirect manually. |
| 407 url = e.info()["location"] | 405 url = e.info()["location"] |
| 408 url_loc = urlparse.urlparse(url) | 406 url_loc = urlparse.urlparse(url) |
| 409 self.host = '%s://%s' % (url_loc[0], url_loc[1]) | 407 self.host = '%s://%s' % (url_loc[0], url_loc[1]) |
| 408 elif e.code >= 500: |
| 409 ErrorExit(e.read()) |
| 410 else: | 410 else: |
| 411 raise | 411 raise |
| 412 finally: | 412 finally: |
| 413 socket.setdefaulttimeout(old_timeout) | 413 socket.setdefaulttimeout(old_timeout) |
| 414 | 414 |
| 415 | 415 |
| 416 class HttpRpcServer(AbstractRpcServer): | 416 class HttpRpcServer(AbstractRpcServer): |
| 417 """Provides a simplified RPC-style interface for HTTP requests.""" | 417 """Provides a simplified RPC-style interface for HTTP requests.""" |
| 418 | 418 |
| 419 def _Authenticate(self): | 419 def _Authenticate(self): |
| (...skipping 105 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 525 dest="save_cookies", default=True, | 525 dest="save_cookies", default=True, |
| 526 help="Do not save authentication cookies to local disk.") | 526 help="Do not save authentication cookies to local disk.") |
| 527 group.add_option("--account_type", action="store", dest="account_type", | 527 group.add_option("--account_type", action="store", dest="account_type", |
| 528 metavar="TYPE", default=AUTH_ACCOUNT_TYPE, | 528 metavar="TYPE", default=AUTH_ACCOUNT_TYPE, |
| 529 choices=["GOOGLE", "HOSTED"], | 529 choices=["GOOGLE", "HOSTED"], |
| 530 help=("Override the default account type " | 530 help=("Override the default account type " |
| 531 "(defaults to '%default', " | 531 "(defaults to '%default', " |
| 532 "valid choices are 'GOOGLE' and 'HOSTED').")) | 532 "valid choices are 'GOOGLE' and 'HOSTED').")) |
| 533 # Issue | 533 # Issue |
| 534 group = parser.add_option_group("Issue options") | 534 group = parser.add_option_group("Issue options") |
| 535 group.add_option("-d", "--description", action="store", dest="description", | 535 group.add_option("-t", "--title", action="store", dest="title", |
| 536 metavar="DESCRIPTION", default=None, | 536 help="New issue subject or new patch set title") |
| 537 help="Optional description when creating an issue.") | 537 group.add_option("-m", "--message", action="store", dest="message", |
| 538 group.add_option("-f", "--description_file", action="store", | |
| 539 dest="description_file", metavar="DESCRIPTION_FILE", | |
| 540 default=None, | 538 default=None, |
| 541 help="Optional path of a file that contains " | 539 help="New issue description or new patch set message") |
| 542 "the description when creating an issue.") | 540 group.add_option("-F", "--file", action="store", dest="file", |
| 541 default=None, help="Read the message above from file.") |
| 543 group.add_option("-r", "--reviewers", action="store", dest="reviewers", | 542 group.add_option("-r", "--reviewers", action="store", dest="reviewers", |
| 544 metavar="REVIEWERS", default=None, | 543 metavar="REVIEWERS", default=None, |
| 545 help="Add reviewers (comma separated email addresses).") | 544 help="Add reviewers (comma separated email addresses).") |
| 546 group.add_option("--cc", action="store", dest="cc", | 545 group.add_option("--cc", action="store", dest="cc", |
| 547 metavar="CC", default=None, | 546 metavar="CC", default=None, |
| 548 help="Add CC (comma separated email addresses).") | 547 help="Add CC (comma separated email addresses).") |
| 549 group.add_option("--private", action="store_true", dest="private", | 548 group.add_option("--private", action="store_true", dest="private", |
| 550 default=False, | 549 default=False, |
| 551 help="Make the issue restricted to reviewers and those CCed") | 550 help="Make the issue restricted to reviewers and those CCed") |
| 552 # Upload options | 551 # Upload options |
| 553 group = parser.add_option_group("Patch options") | 552 group = parser.add_option_group("Patch options") |
| 554 group.add_option("-m", "--message", action="store", dest="message", | |
| 555 metavar="MESSAGE", default=None, | |
| 556 help="A message to identify the patch. " | |
| 557 "Will prompt if omitted.") | |
| 558 group.add_option("-i", "--issue", type="int", action="store", | 553 group.add_option("-i", "--issue", type="int", action="store", |
| 559 metavar="ISSUE", default=None, | 554 metavar="ISSUE", default=None, |
| 560 help="Issue number to which to add. Defaults to new issue.") | 555 help="Issue number to which to add. Defaults to new issue.") |
| 561 group.add_option("--base_url", action="store", dest="base_url", default=None, | 556 group.add_option("--base_url", action="store", dest="base_url", default=None, |
| 562 help="Base URL path for files (listed as \"Base URL\" when " | 557 help="Base URL path for files (listed as \"Base URL\" when " |
| 563 "viewing issue). If omitted, will be guessed automatically " | 558 "viewing issue). If omitted, will be guessed automatically " |
| 564 "for SVN repos and left blank for others.") | 559 "for SVN repos and left blank for others.") |
| 565 group.add_option("--download_base", action="store_true", | 560 group.add_option("--download_base", action="store_true", |
| 566 dest="download_base", default=False, | 561 dest="download_base", default=False, |
| 567 help="Base files will be downloaded by the server " | 562 help="Base files will be downloaded by the server " |
| (...skipping 210 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 778 class VersionControlSystem(object): | 773 class VersionControlSystem(object): |
| 779 """Abstract base class providing an interface to the VCS.""" | 774 """Abstract base class providing an interface to the VCS.""" |
| 780 | 775 |
| 781 def __init__(self, options): | 776 def __init__(self, options): |
| 782 """Constructor. | 777 """Constructor. |
| 783 | 778 |
| 784 Args: | 779 Args: |
| 785 options: Command line options. | 780 options: Command line options. |
| 786 """ | 781 """ |
| 787 self.options = options | 782 self.options = options |
| 788 | 783 |
| 789 def GetGUID(self): | 784 def GetGUID(self): |
| 790 """Return string to distinguish the repository from others, for example to | 785 """Return string to distinguish the repository from others, for example to |
| 791 query all opened review issues for it""" | 786 query all opened review issues for it""" |
| 792 raise NotImplementedError( | 787 raise NotImplementedError( |
| 793 "abstract method -- subclass %s must override" % self.__class__) | 788 "abstract method -- subclass %s must override" % self.__class__) |
| 794 | 789 |
| 795 def PostProcessDiff(self, diff): | 790 def PostProcessDiff(self, diff): |
| 796 """Return the diff with any special post processing this VCS needs, e.g. | 791 """Return the diff with any special post processing this VCS needs, e.g. |
| 797 to include an svn-style "Index:".""" | 792 to include an svn-style "Index:".""" |
| 798 return diff | 793 return diff |
| (...skipping 139 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 938 self.rev_end = match.group(3) | 933 self.rev_end = match.group(3) |
| 939 else: | 934 else: |
| 940 self.rev_start = self.rev_end = None | 935 self.rev_start = self.rev_end = None |
| 941 # Cache output from "svn list -r REVNO dirname". | 936 # Cache output from "svn list -r REVNO dirname". |
| 942 # Keys: dirname, Values: 2-tuple (ouput for start rev and end rev). | 937 # Keys: dirname, Values: 2-tuple (ouput for start rev and end rev). |
| 943 self.svnls_cache = {} | 938 self.svnls_cache = {} |
| 944 # Base URL is required to fetch files deleted in an older revision. | 939 # Base URL is required to fetch files deleted in an older revision. |
| 945 # Result is cached to not guess it over and over again in GetBaseFile(). | 940 # Result is cached to not guess it over and over again in GetBaseFile(). |
| 946 required = self.options.download_base or self.options.revision is not None | 941 required = self.options.download_base or self.options.revision is not None |
| 947 self.svn_base = self._GuessBase(required) | 942 self.svn_base = self._GuessBase(required) |
| 948 | 943 |
| 949 def GetGUID(self): | 944 def GetGUID(self): |
| 950 return self._GetInfo("Repository UUID") | 945 return self._GetInfo("Repository UUID") |
| 951 | 946 |
| 952 def GuessBase(self, required): | 947 def GuessBase(self, required): |
| 953 """Wrapper for _GuessBase.""" | 948 """Wrapper for _GuessBase.""" |
| 954 return self.svn_base | 949 return self.svn_base |
| 955 | 950 |
| 956 def _GuessBase(self, required): | 951 def _GuessBase(self, required): |
| 957 """Returns base URL for current diff. | 952 """Returns base URL for current diff. |
| 958 | 953 |
| (...skipping 14 matching lines...) Expand all Loading... |
| 973 scheme = "http" | 968 scheme = "http" |
| 974 guess = "Google Code " | 969 guess = "Google Code " |
| 975 path = path + "/" | 970 path = path + "/" |
| 976 base = urlparse.urlunparse((scheme, netloc, path, params, | 971 base = urlparse.urlunparse((scheme, netloc, path, params, |
| 977 query, fragment)) | 972 query, fragment)) |
| 978 logging.info("Guessed %sbase = %s", guess, base) | 973 logging.info("Guessed %sbase = %s", guess, base) |
| 979 return base | 974 return base |
| 980 if required: | 975 if required: |
| 981 ErrorExit("Can't find URL in output from svn info") | 976 ErrorExit("Can't find URL in output from svn info") |
| 982 return None | 977 return None |
| 983 | 978 |
| 984 def _GetInfo(self, key): | 979 def _GetInfo(self, key): |
| 985 """Parses 'svn info' for current dir. Returns value for key or None""" | 980 """Parses 'svn info' for current dir. Returns value for key or None""" |
| 986 for line in RunShell(["svn", "info"]).splitlines(): | 981 for line in RunShell(["svn", "info"]).splitlines(): |
| 987 if line.startswith(key + ": "): | 982 if line.startswith(key + ": "): |
| 988 return line.split(":", 1)[1].strip() | 983 return line.split(":", 1)[1].strip() |
| 989 | 984 |
| 990 def _EscapeFilename(self, filename): | 985 def _EscapeFilename(self, filename): |
| 991 """Escapes filename for SVN commands.""" | 986 """Escapes filename for SVN commands.""" |
| 992 if "@" in filename and not filename.endswith("@"): | 987 if "@" in filename and not filename.endswith("@"): |
| 993 filename = "%s@" % filename | 988 filename = "%s@" % filename |
| (...skipping 222 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1216 class GitVCS(VersionControlSystem): | 1211 class GitVCS(VersionControlSystem): |
| 1217 """Implementation of the VersionControlSystem interface for Git.""" | 1212 """Implementation of the VersionControlSystem interface for Git.""" |
| 1218 | 1213 |
| 1219 def __init__(self, options): | 1214 def __init__(self, options): |
| 1220 super(GitVCS, self).__init__(options) | 1215 super(GitVCS, self).__init__(options) |
| 1221 # Map of filename -> (hash before, hash after) of base file. | 1216 # Map of filename -> (hash before, hash after) of base file. |
| 1222 # Hashes for "no such file" are represented as None. | 1217 # Hashes for "no such file" are represented as None. |
| 1223 self.hashes = {} | 1218 self.hashes = {} |
| 1224 # Map of new filename -> old filename for renames. | 1219 # Map of new filename -> old filename for renames. |
| 1225 self.renames = {} | 1220 self.renames = {} |
| 1226 | 1221 |
| 1227 def GetGUID(self): | 1222 def GetGUID(self): |
| 1228 revlist = RunShell("git rev-list --parents HEAD".split()).splitlines() | 1223 revlist = RunShell("git rev-list --parents HEAD".split()).splitlines() |
| 1229 # M-A: Return the 1st root hash, there could be multiple when a | 1224 # M-A: Return the 1st root hash, there could be multiple when a |
| 1230 # subtree is merged. In that case, more analysis would need to | 1225 # subtree is merged. In that case, more analysis would need to |
| 1231 # be done to figure out which HEAD is the 'most representative'. | 1226 # be done to figure out which HEAD is the 'most representative'. |
| 1232 for r in revlist: | 1227 for r in revlist: |
| 1233 if ' ' not in r: | 1228 if ' ' not in r: |
| 1234 return r | 1229 return r |
| 1235 | 1230 |
| 1236 def PostProcessDiff(self, gitdiff): | 1231 def PostProcessDiff(self, gitdiff): |
| (...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1303 # this by overriding the environment (but there is still a problem if the | 1298 # this by overriding the environment (but there is still a problem if the |
| 1304 # git config key "diff.external" is used). | 1299 # git config key "diff.external" is used). |
| 1305 env = os.environ.copy() | 1300 env = os.environ.copy() |
| 1306 if 'GIT_EXTERNAL_DIFF' in env: del env['GIT_EXTERNAL_DIFF'] | 1301 if 'GIT_EXTERNAL_DIFF' in env: del env['GIT_EXTERNAL_DIFF'] |
| 1307 # -M/-C will not print the diff for the deleted file when a file is renamed. | 1302 # -M/-C will not print the diff for the deleted file when a file is renamed. |
| 1308 # This is confusing because the original file will not be shown on the | 1303 # This is confusing because the original file will not be shown on the |
| 1309 # review when a file is renamed. So first get the diff of all deleted files, | 1304 # review when a file is renamed. So first get the diff of all deleted files, |
| 1310 # then the diff of everything except deleted files with rename and copy | 1305 # then the diff of everything except deleted files with rename and copy |
| 1311 # support enabled. | 1306 # support enabled. |
| 1312 cmd = [ | 1307 cmd = [ |
| 1313 "git", "diff", "--no-ext-diff", "--full-index", "--ignore-submodules" | 1308 "git", "diff", "--no-color", "--no-ext-diff", "--full-index", "--ignore-
submodules" |
| 1314 ] | 1309 ] |
| 1315 diff = RunShell(cmd + ["--diff-filter=D"] + extra_args, env=env, | 1310 diff = RunShell(cmd + ["--diff-filter=D"] + extra_args, env=env, |
| 1316 silent_ok=True) | 1311 silent_ok=True) |
| 1317 diff += RunShell(cmd + ["-C", "--diff-filter=ACMRT"] + extra_args, env=env, | 1312 diff += RunShell(cmd + ["-C", "--diff-filter=ACMRT"] + extra_args, env=env, |
| 1318 silent_ok=True) | 1313 silent_ok=True) |
| 1319 if not diff: | 1314 if not diff: |
| 1320 ErrorExit("No output from %s" % (cmd + extra_args)) | 1315 ErrorExit("No output from %s" % (cmd + extra_args)) |
| 1321 return diff | 1316 return diff |
| 1322 | 1317 |
| 1323 def GetUnknownFiles(self): | 1318 def GetUnknownFiles(self): |
| (...skipping 133 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1457 | 1452 |
| 1458 def GetGUID(self): | 1453 def GetGUID(self): |
| 1459 # See chapter "Uniquely identifying a repository" | 1454 # See chapter "Uniquely identifying a repository" |
| 1460 # http://hgbook.red-bean.com/read/customizing-the-output-of-mercurial.html | 1455 # http://hgbook.red-bean.com/read/customizing-the-output-of-mercurial.html |
| 1461 info = RunShell("hg log -r0 --template {node}".split()) | 1456 info = RunShell("hg log -r0 --template {node}".split()) |
| 1462 return info.strip() | 1457 return info.strip() |
| 1463 | 1458 |
| 1464 def _GetRelPath(self, filename): | 1459 def _GetRelPath(self, filename): |
| 1465 """Get relative path of a file according to the current directory, | 1460 """Get relative path of a file according to the current directory, |
| 1466 given its logical path in the repo.""" | 1461 given its logical path in the repo.""" |
| 1467 assert filename.startswith(self.subdir), (filename, self.subdir) | 1462 absname = os.path.join(self.repo_dir, filename) |
| 1468 return filename[len(self.subdir):].lstrip(r"\/") | 1463 return os.path.relpath(absname) |
| 1469 | 1464 |
| 1470 def GenerateDiff(self, extra_args): | 1465 def GenerateDiff(self, extra_args): |
| 1471 cmd = ["hg", "diff", "--git", "-r", self.base_rev] + extra_args | 1466 cmd = [ |
| 1467 "hg", "diff", "--color", "never", "--git", "-r", self.base_rev |
| 1468 ] + extra_args |
| 1472 data = RunShell(cmd, silent_ok=True) | 1469 data = RunShell(cmd, silent_ok=True) |
| 1473 svndiff = [] | 1470 svndiff = [] |
| 1474 filecount = 0 | 1471 filecount = 0 |
| 1475 for line in data.splitlines(): | 1472 for line in data.splitlines(): |
| 1476 m = re.match("diff --git a/(\S+) b/(\S+)", line) | 1473 m = re.match("diff --git a/(\S+) b/(\S+)", line) |
| 1477 if m: | 1474 if m: |
| 1478 # Modify line to make it look like as it comes from svn diff. | 1475 # Modify line to make it look like as it comes from svn diff. |
| 1479 # With this modification no changes on the server side are required | 1476 # With this modification no changes on the server side are required |
| 1480 # to make upload.py work with Mercurial repos. | 1477 # to make upload.py work with Mercurial repos. |
| 1481 # NOTE: for proper handling of moved/copied files, we have to use | 1478 # NOTE: for proper handling of moved/copied files, we have to use |
| 1482 # the second filename. | 1479 # the second filename. |
| 1483 filename = m.group(2) | 1480 filename = m.group(2) |
| 1484 svndiff.append("Index: %s" % filename) | 1481 svndiff.append("Index: %s" % filename) |
| 1485 svndiff.append("=" * 67) | 1482 svndiff.append("=" * 67) |
| 1486 filecount += 1 | 1483 filecount += 1 |
| 1487 logging.info(line) | 1484 logging.info(line) |
| 1488 else: | 1485 else: |
| 1489 svndiff.append(line) | 1486 svndiff.append(line) |
| 1490 if not filecount: | 1487 if not filecount: |
| 1491 ErrorExit("No valid patches found in output from hg diff") | 1488 ErrorExit("No valid patches found in output from hg diff") |
| 1492 return "\n".join(svndiff) + "\n" | 1489 return "\n".join(svndiff) + "\n" |
| 1493 | 1490 |
| 1494 def GetUnknownFiles(self): | 1491 def GetUnknownFiles(self): |
| 1495 """Return a list of files unknown to the VCS.""" | 1492 """Return a list of files unknown to the VCS.""" |
| 1496 args = [] | 1493 args = [] |
| 1497 status = RunShell(["hg", "status", "--rev", self.base_rev, "-u", "."], | 1494 status = RunShell( |
| 1495 ["hg", "status", "--color", "never", "--rev", self.base_rev, "-u", "."], |
| 1498 silent_ok=True) | 1496 silent_ok=True) |
| 1499 unknown_files = [] | 1497 unknown_files = [] |
| 1500 for line in status.splitlines(): | 1498 for line in status.splitlines(): |
| 1501 st, fn = line.split(" ", 1) | 1499 st, fn = line.split(" ", 1) |
| 1502 if st == "?": | 1500 if st == "?": |
| 1503 unknown_files.append(fn) | 1501 unknown_files.append(fn) |
| 1504 return unknown_files | 1502 return unknown_files |
| 1505 | 1503 |
| 1506 def GetBaseFile(self, filename): | 1504 def GetBaseFile(self, filename): |
| 1507 # "hg status" and "hg cat" both take a path relative to the current subdir | 1505 # "hg status" and "hg cat" both take a path relative to the current subdir, |
| 1508 # rather than to the repo root, but "hg diff" has given us the full path | 1506 # but "hg diff" has given us the path relative to the repo root. |
| 1509 # to the repo root. | |
| 1510 base_content = "" | 1507 base_content = "" |
| 1511 new_content = None | 1508 new_content = None |
| 1512 is_binary = False | 1509 is_binary = False |
| 1513 oldrelpath = relpath = self._GetRelPath(filename) | 1510 oldrelpath = relpath = self._GetRelPath(filename) |
| 1514 # "hg status -C" returns two lines for moved/copied files, one otherwise | 1511 # "hg status -C" returns two lines for moved/copied files, one otherwise |
| 1515 out = RunShell(["hg", "status", "-C", "--rev", self.base_rev, relpath]) | 1512 out = RunShell( |
| 1513 [ "hg", "status", "--color", "never", "-C", "--rev", self.base_rev, |
| 1514 relpath]) |
| 1516 out = out.splitlines() | 1515 out = out.splitlines() |
| 1517 # HACK: strip error message about missing file/directory if it isn't in | 1516 # HACK: strip error message about missing file/directory if it isn't in |
| 1518 # the working copy | 1517 # the working copy |
| 1519 if out[0].startswith('%s: ' % relpath): | 1518 if out[0].startswith('%s: ' % relpath): |
| 1520 out = out[1:] | 1519 out = out[1:] |
| 1521 status, _ = out[0].split(' ', 1) | 1520 status, _ = out[0].split(' ', 1) |
| 1522 if len(out) > 1 and status == "A": | 1521 if len(out) > 1 and status == "A": |
| 1523 # Moved/copied => considered as modified, use old filename to | 1522 # Moved/copied => considered as modified, use old filename to |
| 1524 # retrieve base contents | 1523 # retrieve base contents |
| 1525 oldrelpath = out[1].strip() | 1524 oldrelpath = out[1].strip() |
| (...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1568 ErrorExit("A changelist id is required") | 1567 ErrorExit("A changelist id is required") |
| 1569 if (options.revision): | 1568 if (options.revision): |
| 1570 ErrorExit("--rev is not supported for perforce") | 1569 ErrorExit("--rev is not supported for perforce") |
| 1571 | 1570 |
| 1572 self.p4_port = options.p4_port | 1571 self.p4_port = options.p4_port |
| 1573 self.p4_client = options.p4_client | 1572 self.p4_client = options.p4_client |
| 1574 self.p4_user = options.p4_user | 1573 self.p4_user = options.p4_user |
| 1575 | 1574 |
| 1576 ConfirmLogin() | 1575 ConfirmLogin() |
| 1577 | 1576 |
| 1578 if not options.message: | 1577 if not options.title: |
| 1579 description = self.RunPerforceCommand(["describe", self.p4_changelist], | 1578 description = self.RunPerforceCommand(["describe", self.p4_changelist], |
| 1580 marshal_output=True) | 1579 marshal_output=True) |
| 1581 if description and "desc" in description: | 1580 if description and "desc" in description: |
| 1582 # Rietveld doesn't support multi-line descriptions | 1581 # Rietveld doesn't support multi-line descriptions |
| 1583 raw_message = description["desc"].strip() | 1582 raw_title = description["desc"].strip() |
| 1584 lines = raw_message.splitlines() | 1583 lines = raw_title.splitlines() |
| 1585 if len(lines): | 1584 if len(lines): |
| 1586 options.message = lines[0] | 1585 options.title = lines[0] |
| 1587 | 1586 |
| 1588 def GetGUID(self): | 1587 def GetGUID(self): |
| 1589 """For now we don't know how to get repository ID for Perforce""" | 1588 """For now we don't know how to get repository ID for Perforce""" |
| 1590 return | 1589 return |
| 1591 | 1590 |
| 1592 def RunPerforceCommandWithReturnCode(self, extra_args, marshal_output=False, | 1591 def RunPerforceCommandWithReturnCode(self, extra_args, marshal_output=False, |
| 1593 universal_newlines=True): | 1592 universal_newlines=True): |
| 1594 args = ["p4"] | 1593 args = ["p4"] |
| 1595 if marshal_output: | 1594 if marshal_output: |
| 1596 # -G makes perforce format its output as marshalled python objects | 1595 # -G makes perforce format its output as marshalled python objects |
| (...skipping 370 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1967 if errcode != errno.ENOENT: # command not found code | 1966 if errcode != errno.ENOENT: # command not found code |
| 1968 raise | 1967 raise |
| 1969 | 1968 |
| 1970 # Mercurial has a command to get the base directory of a repository | 1969 # Mercurial has a command to get the base directory of a repository |
| 1971 # Try running it, but don't die if we don't have hg installed. | 1970 # Try running it, but don't die if we don't have hg installed. |
| 1972 # NOTE: we try Mercurial first as it can sit on top of an SVN working copy. | 1971 # NOTE: we try Mercurial first as it can sit on top of an SVN working copy. |
| 1973 res = RunDetectCommand(VCS_MERCURIAL, ["hg", "root"]) | 1972 res = RunDetectCommand(VCS_MERCURIAL, ["hg", "root"]) |
| 1974 if res != None: | 1973 if res != None: |
| 1975 return res | 1974 return res |
| 1976 | 1975 |
| 1977 # Subversion has a .svn in all working directories. | 1976 # Subversion from 1.7 has a single centralized .svn folder |
| 1978 if os.path.isdir('.svn'): | 1977 # ( see http://subversion.apache.org/docs/release-notes/1.7.html#wc-ng ) |
| 1979 logging.info("Guessed VCS = Subversion") | 1978 # That's why we use 'svn info' instead of checking for .svn dir |
| 1980 return (VCS_SUBVERSION, None) | 1979 res = RunDetectCommand(VCS_SUBVERSION, ["svn", "info"]) |
| 1980 if res != None: |
| 1981 return res |
| 1981 | 1982 |
| 1982 # Git has a command to test if you're in a git tree. | 1983 # Git has a command to test if you're in a git tree. |
| 1983 # Try running it, but don't die if we don't have git installed. | 1984 # Try running it, but don't die if we don't have git installed. |
| 1984 res = RunDetectCommand(VCS_GIT, ["git", "rev-parse", | 1985 res = RunDetectCommand(VCS_GIT, ["git", "rev-parse", |
| 1985 "--is-inside-work-tree"]) | 1986 "--is-inside-work-tree"]) |
| 1986 if res != None: | 1987 if res != None: |
| 1987 return res | 1988 return res |
| 1988 | 1989 |
| 1989 # detect CVS repos use `cvs status && $? == 0` rules | 1990 # detect CVS repos use `cvs status && $? == 0` rules |
| 1990 res = RunDetectCommand(VCS_CVS, ["cvs", "status"]) | 1991 res = RunDetectCommand(VCS_CVS, ["cvs", "status"]) |
| (...skipping 221 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 2212 if data is None: | 2213 if data is None: |
| 2213 data = vcs.GenerateDiff(args) | 2214 data = vcs.GenerateDiff(args) |
| 2214 data = vcs.PostProcessDiff(data) | 2215 data = vcs.PostProcessDiff(data) |
| 2215 if options.print_diffs: | 2216 if options.print_diffs: |
| 2216 print "Rietveld diff start:*****" | 2217 print "Rietveld diff start:*****" |
| 2217 print data | 2218 print data |
| 2218 print "Rietveld diff end:*****" | 2219 print "Rietveld diff end:*****" |
| 2219 files = vcs.GetBaseFiles(data) | 2220 files = vcs.GetBaseFiles(data) |
| 2220 if verbosity >= 1: | 2221 if verbosity >= 1: |
| 2221 print "Upload server:", options.server, "(change with -s/--server)" | 2222 print "Upload server:", options.server, "(change with -s/--server)" |
| 2222 if options.issue: | |
| 2223 prompt = "Message describing this patch set: " | |
| 2224 else: | |
| 2225 prompt = "New issue subject: " | |
| 2226 message = options.message or raw_input(prompt).strip() | |
| 2227 if not message: | |
| 2228 ErrorExit("A non-empty message is required") | |
| 2229 rpc_server = GetRpcServer(options.server, | 2223 rpc_server = GetRpcServer(options.server, |
| 2230 options.email, | 2224 options.email, |
| 2231 options.host, | 2225 options.host, |
| 2232 options.save_cookies, | 2226 options.save_cookies, |
| 2233 options.account_type) | 2227 options.account_type) |
| 2234 form_fields = [("subject", message)] | 2228 form_fields = [] |
| 2235 | 2229 |
| 2236 repo_guid = vcs.GetGUID() | 2230 repo_guid = vcs.GetGUID() |
| 2237 if repo_guid: | 2231 if repo_guid: |
| 2238 form_fields.append(("repo_guid", repo_guid)) | 2232 form_fields.append(("repo_guid", repo_guid)) |
| 2239 if base: | 2233 if base: |
| 2240 b = urlparse.urlparse(base) | 2234 b = urlparse.urlparse(base) |
| 2241 username, netloc = urllib.splituser(b.netloc) | 2235 username, netloc = urllib.splituser(b.netloc) |
| 2242 if username: | 2236 if username: |
| 2243 logging.info("Removed username from base URL") | 2237 logging.info("Removed username from base URL") |
| 2244 base = urlparse.urlunparse((b.scheme, netloc, b.path, b.params, | 2238 base = urlparse.urlunparse((b.scheme, netloc, b.path, b.params, |
| 2245 b.query, b.fragment)) | 2239 b.query, b.fragment)) |
| 2246 form_fields.append(("base", base)) | 2240 form_fields.append(("base", base)) |
| 2247 if options.issue: | 2241 if options.issue: |
| 2248 form_fields.append(("issue", str(options.issue))) | 2242 form_fields.append(("issue", str(options.issue))) |
| 2249 if options.email: | 2243 if options.email: |
| 2250 form_fields.append(("user", options.email)) | 2244 form_fields.append(("user", options.email)) |
| 2251 if options.reviewers: | 2245 if options.reviewers: |
| 2252 for reviewer in options.reviewers.split(','): | 2246 for reviewer in options.reviewers.split(','): |
| 2253 CheckReviewer(reviewer) | 2247 CheckReviewer(reviewer) |
| 2254 form_fields.append(("reviewers", options.reviewers)) | 2248 form_fields.append(("reviewers", options.reviewers)) |
| 2255 if options.cc: | 2249 if options.cc: |
| 2256 for cc in options.cc.split(','): | 2250 for cc in options.cc.split(','): |
| 2257 CheckReviewer(cc) | 2251 CheckReviewer(cc) |
| 2258 form_fields.append(("cc", options.cc)) | 2252 form_fields.append(("cc", options.cc)) |
| 2259 description = options.description | 2253 |
| 2260 if options.description_file: | 2254 # Process --message, --title and --file. |
| 2261 if options.description: | 2255 message = options.message or "" |
| 2262 ErrorExit("Can't specify description and description_file") | 2256 title = options.title or "" |
| 2263 file = open(options.description_file, 'r') | 2257 if options.file: |
| 2264 description = file.read() | 2258 if options.message: |
| 2259 ErrorExit("Can't specify both message and message file options") |
| 2260 file = open(options.file, 'r') |
| 2261 message = file.read() |
| 2265 file.close() | 2262 file.close() |
| 2266 if description: | 2263 if options.issue: |
| 2267 form_fields.append(("description", description)) | 2264 prompt = "Title describing this patch set: " |
| 2265 else: |
| 2266 prompt = "New issue subject: " |
| 2267 title = ( |
| 2268 title or message.split('\n', 1)[0].strip() or raw_input(prompt).strip()) |
| 2269 if not title and not options.issue: |
| 2270 ErrorExit("A non-empty title is required for a new issue") |
| 2271 # For existing issues, it's fine to give a patchset an empty name. Rietveld |
| 2272 # doesn't accept that so use a whitespace. |
| 2273 title = title or " " |
| 2274 if len(title) > 100: |
| 2275 title = title[:99] + '…' |
| 2276 if title and not options.issue: |
| 2277 message = message or title |
| 2278 |
| 2279 form_fields.append(("subject", title)) |
| 2280 if message: |
| 2281 if not options.issue: |
| 2282 form_fields.append(("description", message)) |
| 2283 else: |
| 2284 # TODO: [ ] Use /<issue>/publish to add a comment. |
| 2285 pass |
| 2286 |
| 2268 # Send a hash of all the base file so the server can determine if a copy | 2287 # Send a hash of all the base file so the server can determine if a copy |
| 2269 # already exists in an earlier patchset. | 2288 # already exists in an earlier patchset. |
| 2270 base_hashes = "" | 2289 base_hashes = "" |
| 2271 for file, info in files.iteritems(): | 2290 for file, info in files.iteritems(): |
| 2272 if not info[0] is None: | 2291 if not info[0] is None: |
| 2273 checksum = md5(info[0]).hexdigest() | 2292 checksum = md5(info[0]).hexdigest() |
| 2274 if base_hashes: | 2293 if base_hashes: |
| 2275 base_hashes += "|" | 2294 base_hashes += "|" |
| 2276 base_hashes += checksum + ":" + file | 2295 base_hashes += checksum + ":" + file |
| 2277 form_fields.append(("base_hashes", base_hashes)) | 2296 form_fields.append(("base_hashes", base_hashes)) |
| 2278 if options.private: | 2297 if options.private: |
| 2279 if options.issue: | 2298 if options.issue: |
| 2280 print "Warning: Private flag ignored when updating an existing issue." | 2299 print "Warning: Private flag ignored when updating an existing issue." |
| 2281 else: | 2300 else: |
| 2282 form_fields.append(("private", "1")) | 2301 form_fields.append(("private", "1")) |
| 2283 if options.send_patch: | 2302 if options.send_patch: |
| 2284 options.send_mail = True | 2303 options.send_mail = True |
| 2285 # If we're uploading base files, don't send the email before the uploads, so | |
| 2286 # that it contains the file status. | |
| 2287 if options.send_mail and options.download_base: | |
| 2288 form_fields.append(("send_mail", "1")) | |
| 2289 if not options.download_base: | 2304 if not options.download_base: |
| 2290 form_fields.append(("content_upload", "1")) | 2305 form_fields.append(("content_upload", "1")) |
| 2291 if len(data) > MAX_UPLOAD_SIZE: | 2306 if len(data) > MAX_UPLOAD_SIZE: |
| 2292 print "Patch is large, so uploading file patches separately." | 2307 print "Patch is large, so uploading file patches separately." |
| 2293 uploaded_diff_file = [] | 2308 uploaded_diff_file = [] |
| 2294 form_fields.append(("separate_patches", "1")) | 2309 form_fields.append(("separate_patches", "1")) |
| 2295 else: | 2310 else: |
| 2296 uploaded_diff_file = [("data", "data.diff", data)] | 2311 uploaded_diff_file = [("data", "data.diff", data)] |
| 2297 ctype, body = EncodeMultipartFormData(form_fields, uploaded_diff_file) | 2312 ctype, body = EncodeMultipartFormData(form_fields, uploaded_diff_file) |
| 2298 response_body = rpc_server.Send("/upload", body, content_type=ctype) | 2313 response_body = rpc_server.Send("/upload", body, content_type=ctype) |
| (...skipping 14 matching lines...) Expand all Loading... |
| 2313 sys.exit(0) | 2328 sys.exit(0) |
| 2314 issue = msg[msg.rfind("/")+1:] | 2329 issue = msg[msg.rfind("/")+1:] |
| 2315 | 2330 |
| 2316 if not uploaded_diff_file: | 2331 if not uploaded_diff_file: |
| 2317 result = UploadSeparatePatches(issue, rpc_server, patchset, data, options) | 2332 result = UploadSeparatePatches(issue, rpc_server, patchset, data, options) |
| 2318 if not options.download_base: | 2333 if not options.download_base: |
| 2319 patches = result | 2334 patches = result |
| 2320 | 2335 |
| 2321 if not options.download_base: | 2336 if not options.download_base: |
| 2322 vcs.UploadBaseFiles(issue, rpc_server, patches, patchset, options, files) | 2337 vcs.UploadBaseFiles(issue, rpc_server, patches, patchset, options, files) |
| 2323 if options.send_mail: | 2338 |
| 2324 payload = "" | 2339 payload = {} # payload for final request |
| 2325 if options.send_patch: | 2340 if options.send_mail: |
| 2326 payload=urllib.urlencode({"attach_patch": "yes"}) | 2341 payload["send_mail"] = "yes" |
| 2327 rpc_server.Send("/" + issue + "/mail", payload=payload) | 2342 if options.send_patch: |
| 2343 payload["attach_patch"] = "yes" |
| 2344 payload = urllib.urlencode(payload) |
| 2345 rpc_server.Send("/" + issue + "/upload_complete/" + (patchset or ""), |
| 2346 payload=payload) |
| 2328 return issue, patchset | 2347 return issue, patchset |
| 2329 | 2348 |
| 2330 | 2349 |
| 2331 def main(): | 2350 def main(): |
| 2332 try: | 2351 try: |
| 2333 logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:" | 2352 logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:" |
| 2334 "%(lineno)s %(message)s ")) | 2353 "%(lineno)s %(message)s ")) |
| 2335 os.environ['LC_ALL'] = 'C' | 2354 os.environ['LC_ALL'] = 'C' |
| 2336 RealMain(sys.argv) | 2355 RealMain(sys.argv) |
| 2337 except KeyboardInterrupt: | 2356 except KeyboardInterrupt: |
| 2338 print | 2357 print |
| 2339 StatusUpdate("Interrupted.") | 2358 StatusUpdate("Interrupted.") |
| 2340 sys.exit(1) | 2359 sys.exit(1) |
| 2341 | 2360 |
| 2342 | 2361 |
| 2343 if __name__ == "__main__": | 2362 if __name__ == "__main__": |
| 2344 main() | 2363 main() |
| OLD | NEW |