| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 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 and Gerrit.""" | 8 """A git-command for integrating reviews on Rietveld and Gerrit.""" |
| 9 | 9 |
| 10 from distutils.version import LooseVersion | 10 from distutils.version import LooseVersion |
| (...skipping 816 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 827 """Returns current branch or None. | 827 """Returns current branch or None. |
| 828 | 828 |
| 829 For refs/heads/* branches, returns just last part. For others, full ref. | 829 For refs/heads/* branches, returns just last part. For others, full ref. |
| 830 """ | 830 """ |
| 831 branchref = GetCurrentBranchRef() | 831 branchref = GetCurrentBranchRef() |
| 832 if branchref: | 832 if branchref: |
| 833 return ShortBranchName(branchref) | 833 return ShortBranchName(branchref) |
| 834 return None | 834 return None |
| 835 | 835 |
| 836 | 836 |
| 837 class _ParsedIssueNumberArgument(object): | |
| 838 def __init__(self, issue=None, patchset=None, hostname=None): | |
| 839 self.issue = issue | |
| 840 self.patchset = patchset | |
| 841 self.hostname = hostname | |
| 842 | |
| 843 @property | |
| 844 def valid(self): | |
| 845 return self.issue is not None | |
| 846 | |
| 847 | |
| 848 class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument): | |
| 849 def __init__(self, *args, **kwargs): | |
| 850 self.patch_url = kwargs.pop('patch_url', None) | |
| 851 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs) | |
| 852 | |
| 853 | |
| 854 def ParseIssueNumberArgument(arg): | |
| 855 """Parses the issue argument and returns _ParsedIssueNumberArgument.""" | |
| 856 fail_result = _ParsedIssueNumberArgument() | |
| 857 | |
| 858 if arg.isdigit(): | |
| 859 return _ParsedIssueNumberArgument(issue=int(arg)) | |
| 860 if not arg.startswith('http'): | |
| 861 return fail_result | |
| 862 url = gclient_utils.UpgradeToHttps(arg) | |
| 863 try: | |
| 864 parsed_url = urlparse.urlparse(url) | |
| 865 except ValueError: | |
| 866 return fail_result | |
| 867 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues(): | |
| 868 tmp = cls.ParseIssueURL(parsed_url) | |
| 869 if tmp is not None: | |
| 870 return tmp | |
| 871 return fail_result | |
| 872 | |
| 873 | |
| 874 class Changelist(object): | 837 class Changelist(object): |
| 875 """Changelist works with one changelist in local branch. | 838 """Changelist works with one changelist in local branch. |
| 876 | 839 |
| 877 Supports two codereview backends: Rietveld or Gerrit, selected at object | 840 Supports two codereview backends: Rietveld or Gerrit, selected at object |
| 878 creation. | 841 creation. |
| 879 | 842 |
| 880 Not safe for concurrent multi-{thread,process} use. | 843 Not safe for concurrent multi-{thread,process} use. |
| 881 """ | 844 """ |
| 882 | 845 |
| 883 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs): | 846 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs): |
| (...skipping 403 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1287 try: | 1250 try: |
| 1288 return presubmit_support.DoPresubmitChecks(change, committing, | 1251 return presubmit_support.DoPresubmitChecks(change, committing, |
| 1289 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin, | 1252 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin, |
| 1290 default_presubmit=None, may_prompt=may_prompt, | 1253 default_presubmit=None, may_prompt=may_prompt, |
| 1291 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit()) | 1254 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit()) |
| 1292 except presubmit_support.PresubmitFailure, e: | 1255 except presubmit_support.PresubmitFailure, e: |
| 1293 DieWithError( | 1256 DieWithError( |
| 1294 ('%s\nMaybe your depot_tools is out of date?\n' | 1257 ('%s\nMaybe your depot_tools is out of date?\n' |
| 1295 'If all fails, contact maruel@') % e) | 1258 'If all fails, contact maruel@') % e) |
| 1296 | 1259 |
| 1297 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory): | |
| 1298 """Fetches and applies the issue patch from codereview to local branch.""" | |
| 1299 if issue_arg.isdigit(): | |
| 1300 parsed_issue_arg = _RietveldParsedIssueNumberArgument(int(issue_arg)) | |
| 1301 else: | |
| 1302 # Assume url. | |
| 1303 parsed_issue_arg = self._codereview_impl.ParseIssueURL( | |
| 1304 urlparse.urlparse(issue_arg)) | |
| 1305 if not parsed_issue_arg or not parsed_issue_arg.valid: | |
| 1306 DieWithError('Failed to parse issue argument "%s". ' | |
| 1307 'Must be an issue number or a valid URL.' % issue_arg) | |
| 1308 return self._codereview_impl.CMDPatchWithParsedIssue( | |
| 1309 parsed_issue_arg, reject, nocommit, directory) | |
| 1310 | |
| 1311 # Forward methods to codereview specific implementation. | 1260 # Forward methods to codereview specific implementation. |
| 1312 | 1261 |
| 1313 def CloseIssue(self): | 1262 def CloseIssue(self): |
| 1314 return self._codereview_impl.CloseIssue() | 1263 return self._codereview_impl.CloseIssue() |
| 1315 | 1264 |
| 1316 def GetStatus(self): | 1265 def GetStatus(self): |
| 1317 return self._codereview_impl.GetStatus() | 1266 return self._codereview_impl.GetStatus() |
| 1318 | 1267 |
| 1319 def GetCodereviewServer(self): | 1268 def GetCodereviewServer(self): |
| 1320 return self._codereview_impl.GetCodereviewServer() | 1269 return self._codereview_impl.GetCodereviewServer() |
| (...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1390 """Returns a list of reviewers approving the change. | 1339 """Returns a list of reviewers approving the change. |
| 1391 | 1340 |
| 1392 Note: not necessarily committers. | 1341 Note: not necessarily committers. |
| 1393 """ | 1342 """ |
| 1394 raise NotImplementedError() | 1343 raise NotImplementedError() |
| 1395 | 1344 |
| 1396 def GetMostRecentPatchset(self): | 1345 def GetMostRecentPatchset(self): |
| 1397 """Returns the most recent patchset number from the codereview site.""" | 1346 """Returns the most recent patchset number from the codereview site.""" |
| 1398 raise NotImplementedError() | 1347 raise NotImplementedError() |
| 1399 | 1348 |
| 1400 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit, | |
| 1401 directory): | |
| 1402 """Fetches and applies the issue. | |
| 1403 | 1349 |
| 1404 Arguments: | |
| 1405 parsed_issue_arg: instance of _ParsedIssueNumberArgument. | |
| 1406 reject: if True, reject the failed patch instead of switching to 3-way | |
| 1407 merge. Rietveld only. | |
| 1408 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld | |
| 1409 only. | |
| 1410 directory: switch to directory before applying the patch. Rietveld only. | |
| 1411 """ | |
| 1412 raise NotImplementedError() | |
| 1413 | |
| 1414 @staticmethod | |
| 1415 def ParseIssueURL(parsed_url): | |
| 1416 """Parses url and returns instance of _ParsedIssueNumberArgument or None if | |
| 1417 failed.""" | |
| 1418 raise NotImplementedError() | |
| 1419 | |
| 1420 | |
| 1421 class _RietveldChangelistImpl(_ChangelistCodereviewBase): | 1350 class _RietveldChangelistImpl(_ChangelistCodereviewBase): |
| 1422 def __init__(self, changelist, auth_config=None, rietveld_server=None): | 1351 def __init__(self, changelist, auth_config=None, rietveld_server=None): |
| 1423 super(_RietveldChangelistImpl, self).__init__(changelist) | 1352 super(_RietveldChangelistImpl, self).__init__(changelist) |
| 1424 assert settings, 'must be initialized in _ChangelistCodereviewBase' | 1353 assert settings, 'must be initialized in _ChangelistCodereviewBase' |
| 1425 settings.GetDefaultServerUrl() | 1354 settings.GetDefaultServerUrl() |
| 1426 | 1355 |
| 1427 self._rietveld_server = rietveld_server | 1356 self._rietveld_server = rietveld_server |
| 1428 self._auth_config = auth_config | 1357 self._auth_config = auth_config |
| 1429 self._props = None | 1358 self._props = None |
| 1430 self._rpc_server = None | 1359 self._rpc_server = None |
| (...skipping 151 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1582 def GetCodereviewServerSetting(self): | 1511 def GetCodereviewServerSetting(self): |
| 1583 """Returns the git setting that stores this change's rietveld server.""" | 1512 """Returns the git setting that stores this change's rietveld server.""" |
| 1584 branch = self.GetBranch() | 1513 branch = self.GetBranch() |
| 1585 if branch: | 1514 if branch: |
| 1586 return 'branch.%s.rietveldserver' % branch | 1515 return 'branch.%s.rietveldserver' % branch |
| 1587 return None | 1516 return None |
| 1588 | 1517 |
| 1589 def GetRieveldObjForPresubmit(self): | 1518 def GetRieveldObjForPresubmit(self): |
| 1590 return self.RpcServer() | 1519 return self.RpcServer() |
| 1591 | 1520 |
| 1592 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit, | |
| 1593 directory): | |
| 1594 # TODO(maruel): Use apply_issue.py | |
| 1595 | |
| 1596 # PatchIssue should never be called with a dirty tree. It is up to the | |
| 1597 # caller to check this, but just in case we assert here since the | |
| 1598 # consequences of the caller not checking this could be dire. | |
| 1599 assert(not git_common.is_dirty_git_tree('apply')) | |
| 1600 assert(parsed_issue_arg.valid) | |
| 1601 self._changelist.issue = parsed_issue_arg.issue | |
| 1602 if parsed_issue_arg.hostname: | |
| 1603 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname | |
| 1604 | |
| 1605 if parsed_issue_arg.patch_url: | |
| 1606 assert parsed_issue_arg.patchset | |
| 1607 patchset = parsed_issue_arg.patchset | |
| 1608 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read() | |
| 1609 else: | |
| 1610 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset() | |
| 1611 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset) | |
| 1612 | |
| 1613 # Switch up to the top-level directory, if necessary, in preparation for | |
| 1614 # applying the patch. | |
| 1615 top = settings.GetRelativeRoot() | |
| 1616 if top: | |
| 1617 os.chdir(top) | |
| 1618 | |
| 1619 # Git patches have a/ at the beginning of source paths. We strip that out | |
| 1620 # with a sed script rather than the -p flag to patch so we can feed either | |
| 1621 # Git or svn-style patches into the same apply command. | |
| 1622 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7. | |
| 1623 try: | |
| 1624 patch_data = subprocess2.check_output( | |
| 1625 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data) | |
| 1626 except subprocess2.CalledProcessError: | |
| 1627 DieWithError('Git patch mungling failed.') | |
| 1628 logging.info(patch_data) | |
| 1629 | |
| 1630 # We use "git apply" to apply the patch instead of "patch" so that we can | |
| 1631 # pick up file adds. | |
| 1632 # The --index flag means: also insert into the index (so we catch adds). | |
| 1633 cmd = ['git', 'apply', '--index', '-p0'] | |
| 1634 if directory: | |
| 1635 cmd.extend(('--directory', directory)) | |
| 1636 if reject: | |
| 1637 cmd.append('--reject') | |
| 1638 elif IsGitVersionAtLeast('1.7.12'): | |
| 1639 cmd.append('--3way') | |
| 1640 try: | |
| 1641 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(), | |
| 1642 stdin=patch_data, stdout=subprocess2.VOID) | |
| 1643 except subprocess2.CalledProcessError: | |
| 1644 print 'Failed to apply the patch' | |
| 1645 return 1 | |
| 1646 | |
| 1647 # If we had an issue, commit the current state and register the issue. | |
| 1648 if not nocommit: | |
| 1649 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' + | |
| 1650 'patch from issue %(i)s at patchset ' | |
| 1651 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)' | |
| 1652 % {'i': self.GetIssue(), 'p': patchset})]) | |
| 1653 self.SetIssue(self.GetIssue()) | |
| 1654 self.SetPatchset(patchset) | |
| 1655 print "Committed patch locally." | |
| 1656 else: | |
| 1657 print "Patch applied to index." | |
| 1658 return 0 | |
| 1659 | |
| 1660 @staticmethod | |
| 1661 def ParseIssueURL(parsed_url): | |
| 1662 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'): | |
| 1663 return None | |
| 1664 # Typical url: https://domain/<issue_number>[/[other]] | |
| 1665 match = re.match('/(\d+)(/.*)?$', parsed_url.path) | |
| 1666 if match: | |
| 1667 return _RietveldParsedIssueNumberArgument( | |
| 1668 issue=int(match.group(1)), | |
| 1669 hostname=parsed_url.netloc) | |
| 1670 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff | |
| 1671 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path) | |
| 1672 if match: | |
| 1673 return _RietveldParsedIssueNumberArgument( | |
| 1674 issue=int(match.group(1)), | |
| 1675 patchset=int(match.group(2)), | |
| 1676 hostname=parsed_url.netloc, | |
| 1677 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl())) | |
| 1678 return None | |
| 1679 | |
| 1680 | 1521 |
| 1681 class _GerritChangelistImpl(_ChangelistCodereviewBase): | 1522 class _GerritChangelistImpl(_ChangelistCodereviewBase): |
| 1682 def __init__(self, changelist, auth_config=None): | 1523 def __init__(self, changelist, auth_config=None): |
| 1683 # auth_config is Rietveld thing, kept here to preserve interface only. | 1524 # auth_config is Rietveld thing, kept here to preserve interface only. |
| 1684 super(_GerritChangelistImpl, self).__init__(changelist) | 1525 super(_GerritChangelistImpl, self).__init__(changelist) |
| 1685 self._change_id = None | 1526 self._change_id = None |
| 1686 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com | 1527 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com |
| 1687 self._gerrit_host = None # e.g. chromium-review.googlesource.com | 1528 self._gerrit_host = None # e.g. chromium-review.googlesource.com |
| 1688 | 1529 |
| 1689 def _GetGerritHost(self): | 1530 def _GetGerritHost(self): |
| (...skipping 158 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1848 may_prompt=not force, | 1689 may_prompt=not force, |
| 1849 verbose=verbose, | 1690 verbose=verbose, |
| 1850 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None)) | 1691 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None)) |
| 1851 if not hook_results.should_continue(): | 1692 if not hook_results.should_continue(): |
| 1852 return 1 | 1693 return 1 |
| 1853 | 1694 |
| 1854 self.SubmitIssue(wait_for_merge=True) | 1695 self.SubmitIssue(wait_for_merge=True) |
| 1855 print('Issue %s has been submitted.' % self.GetIssueURL()) | 1696 print('Issue %s has been submitted.' % self.GetIssueURL()) |
| 1856 return 0 | 1697 return 0 |
| 1857 | 1698 |
| 1858 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit, | |
| 1859 directory): | |
| 1860 assert not reject | |
| 1861 assert not nocommit | |
| 1862 assert not directory | |
| 1863 assert parsed_issue_arg.valid | |
| 1864 | |
| 1865 self._changelist.issue = parsed_issue_arg.issue | |
| 1866 | |
| 1867 if parsed_issue_arg.hostname: | |
| 1868 self._gerrit_host = parsed_issue_arg.hostname | |
| 1869 self._gerrit_server = 'https://%s' % self._gerrit_host | |
| 1870 | |
| 1871 detail = self._GetChangeDetail(['ALL_REVISIONS']) | |
| 1872 | |
| 1873 if not parsed_issue_arg.patchset: | |
| 1874 # Use current revision by default. | |
| 1875 revision_info = detail['revisions'][detail['current_revision']] | |
| 1876 patchset = int(revision_info['_number']) | |
| 1877 else: | |
| 1878 patchset = parsed_issue_arg.patchset | |
| 1879 for revision_info in detail['revisions'].itervalues(): | |
| 1880 if int(revision_info['_number']) == parsed_issue_arg.patchset: | |
| 1881 break | |
| 1882 else: | |
| 1883 DieWithError('Couldn\'t find patchset %i in issue %i' % | |
| 1884 (parsed_issue_arg.patchset, self.GetIssue())) | |
| 1885 | |
| 1886 fetch_info = revision_info['fetch']['http'] | |
| 1887 RunGit(['fetch', fetch_info['url'], fetch_info['ref']]) | |
| 1888 RunGit(['cherry-pick', 'FETCH_HEAD']) | |
| 1889 self.SetIssue(self.GetIssue()) | |
| 1890 self.SetPatchset(patchset) | |
| 1891 print('Committed patch for issue %i pathset %i locally' % | |
| 1892 (self.GetIssue(), self.GetPatchset())) | |
| 1893 return 0 | |
| 1894 | |
| 1895 @staticmethod | |
| 1896 def ParseIssueURL(parsed_url): | |
| 1897 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'): | |
| 1898 return None | |
| 1899 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]] | |
| 1900 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]] | |
| 1901 # Short urls like https://domain/<issue_number> can be used, but don't allow | |
| 1902 # specifying the patchset (you'd 404), but we allow that here. | |
| 1903 if parsed_url.path == '/': | |
| 1904 part = parsed_url.fragment | |
| 1905 else: | |
| 1906 part = parsed_url.path | |
| 1907 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part) | |
| 1908 if match: | |
| 1909 return _ParsedIssueNumberArgument( | |
| 1910 issue=int(match.group(2)), | |
| 1911 patchset=int(match.group(4)) if match.group(4) else None, | |
| 1912 hostname=parsed_url.netloc) | |
| 1913 return None | |
| 1914 | |
| 1915 | 1699 |
| 1916 _CODEREVIEW_IMPLEMENTATIONS = { | 1700 _CODEREVIEW_IMPLEMENTATIONS = { |
| 1917 'rietveld': _RietveldChangelistImpl, | 1701 'rietveld': _RietveldChangelistImpl, |
| 1918 'gerrit': _GerritChangelistImpl, | 1702 'gerrit': _GerritChangelistImpl, |
| 1919 } | 1703 } |
| 1920 | 1704 |
| 1921 | 1705 |
| 1922 class ChangeDescription(object): | 1706 class ChangeDescription(object): |
| 1923 """Contains a parsed form of the change description.""" | 1707 """Contains a parsed form of the change description.""" |
| 1924 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$' | 1708 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$' |
| (...skipping 1877 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 3802 def CMDland(parser, args): | 3586 def CMDland(parser, args): |
| 3803 """Commits the current changelist via git.""" | 3587 """Commits the current changelist via git.""" |
| 3804 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id(): | 3588 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id(): |
| 3805 print('This appears to be an SVN repository.') | 3589 print('This appears to be an SVN repository.') |
| 3806 print('Are you sure you didn\'t mean \'git cl dcommit\'?') | 3590 print('Are you sure you didn\'t mean \'git cl dcommit\'?') |
| 3807 print('(Ignore if this is the first commit after migrating from svn->git)') | 3591 print('(Ignore if this is the first commit after migrating from svn->git)') |
| 3808 ask_for_data('[Press enter to push or ctrl-C to quit]') | 3592 ask_for_data('[Press enter to push or ctrl-C to quit]') |
| 3809 return SendUpstream(parser, args, 'land') | 3593 return SendUpstream(parser, args, 'land') |
| 3810 | 3594 |
| 3811 | 3595 |
| 3596 def ParseIssueNum(arg): |
| 3597 """Parses the issue number from args if present otherwise returns None.""" |
| 3598 if re.match(r'\d+', arg): |
| 3599 return arg |
| 3600 if arg.startswith('http'): |
| 3601 return re.sub(r'.*/(\d+)/?', r'\1', arg) |
| 3602 return None |
| 3603 |
| 3604 |
| 3812 @subcommand.usage('<patch url or issue id or issue url>') | 3605 @subcommand.usage('<patch url or issue id or issue url>') |
| 3813 def CMDpatch(parser, args): | 3606 def CMDpatch(parser, args): |
| 3814 """Patches in a code review.""" | 3607 """Patches in a code review.""" |
| 3815 parser.add_option('-b', dest='newbranch', | 3608 parser.add_option('-b', dest='newbranch', |
| 3816 help='create a new branch off trunk for the patch') | 3609 help='create a new branch off trunk for the patch') |
| 3817 parser.add_option('-f', '--force', action='store_true', | 3610 parser.add_option('-f', '--force', action='store_true', |
| 3818 help='with -b, clobber any existing branch') | 3611 help='with -b, clobber any existing branch') |
| 3819 parser.add_option('-d', '--directory', action='store', metavar='DIR', | 3612 parser.add_option('-d', '--directory', action='store', metavar='DIR', |
| 3820 help='Change to the directory DIR immediately, ' | 3613 help='Change to the directory DIR immediately, ' |
| 3821 'before doing anything else. Rietveld only.') | 3614 'before doing anything else.') |
| 3822 parser.add_option('--reject', action='store_true', | 3615 parser.add_option('--reject', action='store_true', |
| 3823 help='failed patches spew .rej files rather than ' | 3616 help='failed patches spew .rej files rather than ' |
| 3824 'attempting a 3-way merge. Rietveld only.') | 3617 'attempting a 3-way merge') |
| 3825 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit', | 3618 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit', |
| 3826 help='don\'t commit after patch applies. Rietveld only.') | 3619 help="don't commit after patch applies") |
| 3827 | 3620 |
| 3828 | 3621 group = optparse.OptionGroup(parser, |
| 3829 group = optparse.OptionGroup( | 3622 """Options for continuing work on the current issue uploaded |
| 3830 parser, | 3623 from a different clone (e.g. different machine). Must be used independently from |
| 3831 'Options for continuing work on the current issue uploaded from a ' | 3624 the other options. No issue number should be specified, and the branch must have |
| 3832 'different clone (e.g. different machine). Must be used independently ' | 3625 an issue number associated with it""") |
| 3833 'from the other options. No issue number should be specified, and the ' | 3626 group.add_option('--reapply', action='store_true', |
| 3834 'branch must have an issue number associated with it') | 3627 dest='reapply', |
| 3835 group.add_option('--reapply', action='store_true', dest='reapply', | 3628 help="""Reset the branch and reapply the issue. |
| 3836 help='Reset the branch and reapply the issue.\n' | 3629 CAUTION: This will undo any local changes in this branch""") |
| 3837 'CAUTION: This will undo any local changes in this ' | |
| 3838 'branch') | |
| 3839 | 3630 |
| 3840 group.add_option('--pull', action='store_true', dest='pull', | 3631 group.add_option('--pull', action='store_true', dest='pull', |
| 3841 help='Performs a pull before reapplying.') | 3632 help="Performs a pull before reapplying.") |
| 3842 parser.add_option_group(group) | 3633 parser.add_option_group(group) |
| 3843 | 3634 |
| 3844 auth.add_auth_options(parser) | 3635 auth.add_auth_options(parser) |
| 3845 (options, args) = parser.parse_args(args) | 3636 (options, args) = parser.parse_args(args) |
| 3846 auth_config = auth.extract_auth_config_from_options(options) | 3637 auth_config = auth.extract_auth_config_from_options(options) |
| 3847 | 3638 |
| 3848 cl = Changelist(auth_config=auth_config) | |
| 3849 | |
| 3850 issue_arg = None | 3639 issue_arg = None |
| 3851 if options.reapply : | 3640 if options.reapply : |
| 3852 if len(args) > 0: | 3641 if len(args) > 0: |
| 3853 parser.error('--reapply implies no additional arguments.') | 3642 parser.error("--reapply implies no additional arguments.") |
| 3854 | 3643 |
| 3644 cl = Changelist() |
| 3855 issue_arg = cl.GetIssue() | 3645 issue_arg = cl.GetIssue() |
| 3856 upstream = cl.GetUpstreamBranch() | 3646 upstream = cl.GetUpstreamBranch() |
| 3857 if upstream == None: | 3647 if upstream == None: |
| 3858 parser.error('No upstream branch specified. Cannot reset branch') | 3648 parser.error("No upstream branch specified. Cannot reset branch") |
| 3859 | 3649 |
| 3860 RunGit(['reset', '--hard', upstream]) | 3650 RunGit(['reset', '--hard', upstream]) |
| 3861 if options.pull: | 3651 if options.pull: |
| 3862 RunGit(['pull']) | 3652 RunGit(['pull']) |
| 3863 else: | 3653 else: |
| 3864 if len(args) != 1: | 3654 if len(args) != 1: |
| 3865 parser.error('Must specify issue number or url') | 3655 parser.error("Must specify issue number") |
| 3866 issue_arg = args[0] | |
| 3867 | 3656 |
| 3868 if not issue_arg: | 3657 issue_arg = ParseIssueNum(args[0]) |
| 3658 |
| 3659 # The patch URL works because ParseIssueNum won't do any substitution |
| 3660 # as the re.sub pattern fails to match and just returns it. |
| 3661 if issue_arg == None: |
| 3869 parser.print_help() | 3662 parser.print_help() |
| 3870 return 1 | 3663 return 1 |
| 3871 | 3664 |
| 3872 if cl.IsGerrit(): | |
| 3873 if options.reject: | |
| 3874 parser.error('--reject is not supported with Gerrit codereview.') | |
| 3875 if options.nocommit: | |
| 3876 parser.error('--nocommit is not supported with Gerrit codereview.') | |
| 3877 if options.directory: | |
| 3878 parser.error('--directory is not supported with Gerrit codereview.') | |
| 3879 | |
| 3880 # We don't want uncommitted changes mixed up with the patch. | 3665 # We don't want uncommitted changes mixed up with the patch. |
| 3881 if git_common.is_dirty_git_tree('patch'): | 3666 if git_common.is_dirty_git_tree('patch'): |
| 3882 return 1 | 3667 return 1 |
| 3883 | 3668 |
| 3669 # TODO(maruel): Use apply_issue.py |
| 3670 # TODO(ukai): use gerrit-cherry-pick for gerrit repository? |
| 3671 |
| 3884 if options.newbranch: | 3672 if options.newbranch: |
| 3885 if options.reapply: | 3673 if options.reapply: |
| 3886 parser.error("--reapply excludes any option other than --pull") | 3674 parser.error("--reapply excludes any option other than --pull") |
| 3887 if options.force: | 3675 if options.force: |
| 3888 RunGit(['branch', '-D', options.newbranch], | 3676 RunGit(['branch', '-D', options.newbranch], |
| 3889 stderr=subprocess2.PIPE, error_ok=True) | 3677 stderr=subprocess2.PIPE, error_ok=True) |
| 3890 RunGit(['checkout', '-b', options.newbranch, | 3678 RunGit(['checkout', '-b', options.newbranch, |
| 3891 Changelist().GetUpstreamBranch()]) | 3679 Changelist().GetUpstreamBranch()]) |
| 3892 | 3680 |
| 3893 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit, | 3681 return PatchIssue(issue_arg, options.reject, options.nocommit, |
| 3894 options.directory) | 3682 options.directory, auth_config) |
| 3683 |
| 3684 |
| 3685 def PatchIssue(issue_arg, reject, nocommit, directory, auth_config): |
| 3686 # PatchIssue should never be called with a dirty tree. It is up to the |
| 3687 # caller to check this, but just in case we assert here since the |
| 3688 # consequences of the caller not checking this could be dire. |
| 3689 assert(not git_common.is_dirty_git_tree('apply')) |
| 3690 |
| 3691 # TODO(tandrii): implement for Gerrit. |
| 3692 if type(issue_arg) is int or issue_arg.isdigit(): |
| 3693 # Input is an issue id. Figure out the URL. |
| 3694 issue = int(issue_arg) |
| 3695 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config) |
| 3696 patchset = cl.GetMostRecentPatchset() |
| 3697 patch_data = cl._codereview_impl.GetPatchSetDiff(issue, patchset) |
| 3698 else: |
| 3699 # Assume it's a URL to the patch. Default to https. |
| 3700 issue_url = gclient_utils.UpgradeToHttps(issue_arg) |
| 3701 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url) |
| 3702 if not match: |
| 3703 DieWithError('Must pass an issue ID or full URL for ' |
| 3704 '\'Download raw patch set\'') |
| 3705 issue = int(match.group(2)) |
| 3706 cl = Changelist(issue=issue, codereview='rietveld', |
| 3707 rietveld_server=match.group(1), auth_config=auth_config) |
| 3708 patchset = int(match.group(3)) |
| 3709 patch_data = urllib2.urlopen(issue_arg).read() |
| 3710 |
| 3711 # Switch up to the top-level directory, if necessary, in preparation for |
| 3712 # applying the patch. |
| 3713 top = settings.GetRelativeRoot() |
| 3714 if top: |
| 3715 os.chdir(top) |
| 3716 |
| 3717 # Git patches have a/ at the beginning of source paths. We strip that out |
| 3718 # with a sed script rather than the -p flag to patch so we can feed either |
| 3719 # Git or svn-style patches into the same apply command. |
| 3720 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7. |
| 3721 try: |
| 3722 patch_data = subprocess2.check_output( |
| 3723 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data) |
| 3724 except subprocess2.CalledProcessError: |
| 3725 DieWithError('Git patch mungling failed.') |
| 3726 logging.info(patch_data) |
| 3727 |
| 3728 # We use "git apply" to apply the patch instead of "patch" so that we can |
| 3729 # pick up file adds. |
| 3730 # The --index flag means: also insert into the index (so we catch adds). |
| 3731 cmd = ['git', 'apply', '--index', '-p0'] |
| 3732 if directory: |
| 3733 cmd.extend(('--directory', directory)) |
| 3734 if reject: |
| 3735 cmd.append('--reject') |
| 3736 elif IsGitVersionAtLeast('1.7.12'): |
| 3737 cmd.append('--3way') |
| 3738 try: |
| 3739 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(), |
| 3740 stdin=patch_data, stdout=subprocess2.VOID) |
| 3741 except subprocess2.CalledProcessError: |
| 3742 print 'Failed to apply the patch' |
| 3743 return 1 |
| 3744 |
| 3745 # If we had an issue, commit the current state and register the issue. |
| 3746 if not nocommit: |
| 3747 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' + |
| 3748 'patch from issue %(i)s at patchset ' |
| 3749 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)' |
| 3750 % {'i': issue, 'p': patchset})]) |
| 3751 cl = Changelist(codereview='rietveld', auth_config=auth_config, |
| 3752 rietveld_server=cl.GetCodereviewServer()) |
| 3753 cl.SetIssue(issue) |
| 3754 cl.SetPatchset(patchset) |
| 3755 print "Committed patch locally." |
| 3756 else: |
| 3757 print "Patch applied to index." |
| 3758 return 0 |
| 3895 | 3759 |
| 3896 | 3760 |
| 3897 def CMDrebase(parser, args): | 3761 def CMDrebase(parser, args): |
| 3898 """Rebases current branch on top of svn repo.""" | 3762 """Rebases current branch on top of svn repo.""" |
| 3899 # Provide a wrapper for git svn rebase to help avoid accidental | 3763 # Provide a wrapper for git svn rebase to help avoid accidental |
| 3900 # git svn dcommit. | 3764 # git svn dcommit. |
| 3901 # It's the only command that doesn't use parser at all since we just defer | 3765 # It's the only command that doesn't use parser at all since we just defer |
| 3902 # execution to git-svn. | 3766 # execution to git-svn. |
| 3903 | 3767 |
| 3904 return RunGitWithCode(['svn', 'rebase'] + args)[1] | 3768 return RunGitWithCode(['svn', 'rebase'] + args)[1] |
| (...skipping 381 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 4286 | 4150 |
| 4287 def CMDdiff(parser, args): | 4151 def CMDdiff(parser, args): |
| 4288 """Shows differences between local tree and last upload.""" | 4152 """Shows differences between local tree and last upload.""" |
| 4289 auth.add_auth_options(parser) | 4153 auth.add_auth_options(parser) |
| 4290 options, args = parser.parse_args(args) | 4154 options, args = parser.parse_args(args) |
| 4291 auth_config = auth.extract_auth_config_from_options(options) | 4155 auth_config = auth.extract_auth_config_from_options(options) |
| 4292 if args: | 4156 if args: |
| 4293 parser.error('Unrecognized args: %s' % ' '.join(args)) | 4157 parser.error('Unrecognized args: %s' % ' '.join(args)) |
| 4294 | 4158 |
| 4295 # Uncommitted (staged and unstaged) changes will be destroyed by | 4159 # Uncommitted (staged and unstaged) changes will be destroyed by |
| 4296 # "git reset --hard" if there are merging conflicts in CMDPatchIssue(). | 4160 # "git reset --hard" if there are merging conflicts in PatchIssue(). |
| 4297 # Staged changes would be committed along with the patch from last | 4161 # Staged changes would be committed along with the patch from last |
| 4298 # upload, hence counted toward the "last upload" side in the final | 4162 # upload, hence counted toward the "last upload" side in the final |
| 4299 # diff output, and this is not what we want. | 4163 # diff output, and this is not what we want. |
| 4300 if git_common.is_dirty_git_tree('diff'): | 4164 if git_common.is_dirty_git_tree('diff'): |
| 4301 return 1 | 4165 return 1 |
| 4302 | 4166 |
| 4303 cl = Changelist(auth_config=auth_config) | 4167 cl = Changelist(auth_config=auth_config) |
| 4304 issue = cl.GetIssue() | 4168 issue = cl.GetIssue() |
| 4305 branch = cl.GetBranch() | 4169 branch = cl.GetBranch() |
| 4306 if not issue: | 4170 if not issue: |
| 4307 DieWithError('No issue found for current branch (%s)' % branch) | 4171 DieWithError('No issue found for current branch (%s)' % branch) |
| 4308 TMP_BRANCH = 'git-cl-diff' | 4172 TMP_BRANCH = 'git-cl-diff' |
| 4309 base_branch = cl.GetCommonAncestorWithUpstream() | 4173 base_branch = cl.GetCommonAncestorWithUpstream() |
| 4310 | 4174 |
| 4311 # Create a new branch based on the merge-base | 4175 # Create a new branch based on the merge-base |
| 4312 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch]) | 4176 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch]) |
| 4313 try: | 4177 try: |
| 4314 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None) | 4178 # Patch in the latest changes from rietveld. |
| 4179 rtn = PatchIssue(issue, False, False, None, auth_config) |
| 4315 if rtn != 0: | 4180 if rtn != 0: |
| 4316 RunGit(['reset', '--hard']) | 4181 RunGit(['reset', '--hard']) |
| 4317 return rtn | 4182 return rtn |
| 4318 | 4183 |
| 4319 # Switch back to starting branch and diff against the temporary | 4184 # Switch back to starting branch and diff against the temporary |
| 4320 # branch containing the latest rietveld patch. | 4185 # branch containing the latest rietveld patch. |
| 4321 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--']) | 4186 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--']) |
| 4322 finally: | 4187 finally: |
| 4323 RunGit(['checkout', '-q', branch]) | 4188 RunGit(['checkout', '-q', branch]) |
| 4324 RunGit(['branch', '-D', TMP_BRANCH]) | 4189 RunGit(['branch', '-D', TMP_BRANCH]) |
| (...skipping 187 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 4512 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir) | 4377 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir) |
| 4513 if opts.diff: | 4378 if opts.diff: |
| 4514 sys.stdout.write(stdout) | 4379 sys.stdout.write(stdout) |
| 4515 | 4380 |
| 4516 return return_value | 4381 return return_value |
| 4517 | 4382 |
| 4518 | 4383 |
| 4519 @subcommand.usage('<codereview url or issue id>') | 4384 @subcommand.usage('<codereview url or issue id>') |
| 4520 def CMDcheckout(parser, args): | 4385 def CMDcheckout(parser, args): |
| 4521 """Checks out a branch associated with a given Rietveld issue.""" | 4386 """Checks out a branch associated with a given Rietveld issue.""" |
| 4522 # TODO(tandrii): consider adding this for Gerrit? | |
| 4523 _, args = parser.parse_args(args) | 4387 _, args = parser.parse_args(args) |
| 4524 | 4388 |
| 4525 if len(args) != 1: | 4389 if len(args) != 1: |
| 4526 parser.print_help() | 4390 parser.print_help() |
| 4527 return 1 | 4391 return 1 |
| 4528 | 4392 |
| 4529 issue_arg = ParseIssueNumberArgument(args[0]) | 4393 target_issue = ParseIssueNum(args[0]) |
| 4530 if issue_arg.valid: | 4394 if target_issue == None: |
| 4531 parser.print_help() | 4395 parser.print_help() |
| 4532 return 1 | 4396 return 1 |
| 4533 target_issue = issue_arg.issue | |
| 4534 | 4397 |
| 4535 key_and_issues = [x.split() for x in RunGit( | 4398 key_and_issues = [x.split() for x in RunGit( |
| 4536 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue']) | 4399 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue']) |
| 4537 .splitlines()] | 4400 .splitlines()] |
| 4538 branches = [] | 4401 branches = [] |
| 4539 for key, issue in key_and_issues: | 4402 for key, issue in key_and_issues: |
| 4540 if issue == target_issue: | 4403 if issue == target_issue: |
| 4541 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key)) | 4404 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key)) |
| 4542 | 4405 |
| 4543 if len(branches) == 0: | 4406 if len(branches) == 0: |
| (...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 4614 if __name__ == '__main__': | 4477 if __name__ == '__main__': |
| 4615 # These affect sys.stdout so do it outside of main() to simplify mocks in | 4478 # These affect sys.stdout so do it outside of main() to simplify mocks in |
| 4616 # unit testing. | 4479 # unit testing. |
| 4617 fix_encoding.fix_encoding() | 4480 fix_encoding.fix_encoding() |
| 4618 colorama.init(wrap="TERM" not in os.environ) | 4481 colorama.init(wrap="TERM" not in os.environ) |
| 4619 try: | 4482 try: |
| 4620 sys.exit(main(sys.argv[1:])) | 4483 sys.exit(main(sys.argv[1:])) |
| 4621 except KeyboardInterrupt: | 4484 except KeyboardInterrupt: |
| 4622 sys.stderr.write('interrupted\n') | 4485 sys.stderr.write('interrupted\n') |
| 4623 sys.exit(1) | 4486 sys.exit(1) |
| OLD | NEW |