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 |
837 class Changelist(object): | 874 class Changelist(object): |
838 """Changelist works with one changelist in local branch. | 875 """Changelist works with one changelist in local branch. |
839 | 876 |
840 Supports two codereview backends: Rietveld or Gerrit, selected at object | 877 Supports two codereview backends: Rietveld or Gerrit, selected at object |
841 creation. | 878 creation. |
842 | 879 |
843 Not safe for concurrent multi-{thread,process} use. | 880 Not safe for concurrent multi-{thread,process} use. |
844 """ | 881 """ |
845 | 882 |
846 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs): | 883 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs): |
(...skipping 403 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1250 try: | 1287 try: |
1251 return presubmit_support.DoPresubmitChecks(change, committing, | 1288 return presubmit_support.DoPresubmitChecks(change, committing, |
1252 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin, | 1289 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin, |
1253 default_presubmit=None, may_prompt=may_prompt, | 1290 default_presubmit=None, may_prompt=may_prompt, |
1254 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit()) | 1291 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit()) |
1255 except presubmit_support.PresubmitFailure, e: | 1292 except presubmit_support.PresubmitFailure, e: |
1256 DieWithError( | 1293 DieWithError( |
1257 ('%s\nMaybe your depot_tools is out of date?\n' | 1294 ('%s\nMaybe your depot_tools is out of date?\n' |
1258 'If all fails, contact maruel@') % e) | 1295 'If all fails, contact maruel@') % e) |
1259 | 1296 |
| 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 |
1260 # Forward methods to codereview specific implementation. | 1311 # Forward methods to codereview specific implementation. |
1261 | 1312 |
1262 def CloseIssue(self): | 1313 def CloseIssue(self): |
1263 return self._codereview_impl.CloseIssue() | 1314 return self._codereview_impl.CloseIssue() |
1264 | 1315 |
1265 def GetStatus(self): | 1316 def GetStatus(self): |
1266 return self._codereview_impl.GetStatus() | 1317 return self._codereview_impl.GetStatus() |
1267 | 1318 |
1268 def GetCodereviewServer(self): | 1319 def GetCodereviewServer(self): |
1269 return self._codereview_impl.GetCodereviewServer() | 1320 return self._codereview_impl.GetCodereviewServer() |
(...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1339 """Returns a list of reviewers approving the change. | 1390 """Returns a list of reviewers approving the change. |
1340 | 1391 |
1341 Note: not necessarily committers. | 1392 Note: not necessarily committers. |
1342 """ | 1393 """ |
1343 raise NotImplementedError() | 1394 raise NotImplementedError() |
1344 | 1395 |
1345 def GetMostRecentPatchset(self): | 1396 def GetMostRecentPatchset(self): |
1346 """Returns the most recent patchset number from the codereview site.""" | 1397 """Returns the most recent patchset number from the codereview site.""" |
1347 raise NotImplementedError() | 1398 raise NotImplementedError() |
1348 | 1399 |
| 1400 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit, |
| 1401 directory): |
| 1402 """Fetches and applies the issue. |
1349 | 1403 |
| 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 |
1350 class _RietveldChangelistImpl(_ChangelistCodereviewBase): | 1421 class _RietveldChangelistImpl(_ChangelistCodereviewBase): |
1351 def __init__(self, changelist, auth_config=None, rietveld_server=None): | 1422 def __init__(self, changelist, auth_config=None, rietveld_server=None): |
1352 super(_RietveldChangelistImpl, self).__init__(changelist) | 1423 super(_RietveldChangelistImpl, self).__init__(changelist) |
1353 assert settings, 'must be initialized in _ChangelistCodereviewBase' | 1424 assert settings, 'must be initialized in _ChangelistCodereviewBase' |
1354 settings.GetDefaultServerUrl() | 1425 settings.GetDefaultServerUrl() |
1355 | 1426 |
1356 self._rietveld_server = rietveld_server | 1427 self._rietveld_server = rietveld_server |
1357 self._auth_config = auth_config | 1428 self._auth_config = auth_config |
1358 self._props = None | 1429 self._props = None |
1359 self._rpc_server = None | 1430 self._rpc_server = None |
(...skipping 151 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1511 def GetCodereviewServerSetting(self): | 1582 def GetCodereviewServerSetting(self): |
1512 """Returns the git setting that stores this change's rietveld server.""" | 1583 """Returns the git setting that stores this change's rietveld server.""" |
1513 branch = self.GetBranch() | 1584 branch = self.GetBranch() |
1514 if branch: | 1585 if branch: |
1515 return 'branch.%s.rietveldserver' % branch | 1586 return 'branch.%s.rietveldserver' % branch |
1516 return None | 1587 return None |
1517 | 1588 |
1518 def GetRieveldObjForPresubmit(self): | 1589 def GetRieveldObjForPresubmit(self): |
1519 return self.RpcServer() | 1590 return self.RpcServer() |
1520 | 1591 |
| 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 |
1521 | 1680 |
1522 class _GerritChangelistImpl(_ChangelistCodereviewBase): | 1681 class _GerritChangelistImpl(_ChangelistCodereviewBase): |
1523 def __init__(self, changelist, auth_config=None): | 1682 def __init__(self, changelist, auth_config=None): |
1524 # auth_config is Rietveld thing, kept here to preserve interface only. | 1683 # auth_config is Rietveld thing, kept here to preserve interface only. |
1525 super(_GerritChangelistImpl, self).__init__(changelist) | 1684 super(_GerritChangelistImpl, self).__init__(changelist) |
1526 self._change_id = None | 1685 self._change_id = None |
1527 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com | 1686 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com |
1528 self._gerrit_host = None # e.g. chromium-review.googlesource.com | 1687 self._gerrit_host = None # e.g. chromium-review.googlesource.com |
1529 | 1688 |
1530 def _GetGerritHost(self): | 1689 def _GetGerritHost(self): |
(...skipping 158 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1689 may_prompt=not force, | 1848 may_prompt=not force, |
1690 verbose=verbose, | 1849 verbose=verbose, |
1691 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None)) | 1850 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None)) |
1692 if not hook_results.should_continue(): | 1851 if not hook_results.should_continue(): |
1693 return 1 | 1852 return 1 |
1694 | 1853 |
1695 self.SubmitIssue(wait_for_merge=True) | 1854 self.SubmitIssue(wait_for_merge=True) |
1696 print('Issue %s has been submitted.' % self.GetIssueURL()) | 1855 print('Issue %s has been submitted.' % self.GetIssueURL()) |
1697 return 0 | 1856 return 0 |
1698 | 1857 |
| 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 |
1699 | 1915 |
1700 _CODEREVIEW_IMPLEMENTATIONS = { | 1916 _CODEREVIEW_IMPLEMENTATIONS = { |
1701 'rietveld': _RietveldChangelistImpl, | 1917 'rietveld': _RietveldChangelistImpl, |
1702 'gerrit': _GerritChangelistImpl, | 1918 'gerrit': _GerritChangelistImpl, |
1703 } | 1919 } |
1704 | 1920 |
1705 | 1921 |
1706 class ChangeDescription(object): | 1922 class ChangeDescription(object): |
1707 """Contains a parsed form of the change description.""" | 1923 """Contains a parsed form of the change description.""" |
1708 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$' | 1924 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$' |
(...skipping 1877 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
3586 def CMDland(parser, args): | 3802 def CMDland(parser, args): |
3587 """Commits the current changelist via git.""" | 3803 """Commits the current changelist via git.""" |
3588 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id(): | 3804 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id(): |
3589 print('This appears to be an SVN repository.') | 3805 print('This appears to be an SVN repository.') |
3590 print('Are you sure you didn\'t mean \'git cl dcommit\'?') | 3806 print('Are you sure you didn\'t mean \'git cl dcommit\'?') |
3591 print('(Ignore if this is the first commit after migrating from svn->git)') | 3807 print('(Ignore if this is the first commit after migrating from svn->git)') |
3592 ask_for_data('[Press enter to push or ctrl-C to quit]') | 3808 ask_for_data('[Press enter to push or ctrl-C to quit]') |
3593 return SendUpstream(parser, args, 'land') | 3809 return SendUpstream(parser, args, 'land') |
3594 | 3810 |
3595 | 3811 |
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 | |
3605 @subcommand.usage('<patch url or issue id or issue url>') | 3812 @subcommand.usage('<patch url or issue id or issue url>') |
3606 def CMDpatch(parser, args): | 3813 def CMDpatch(parser, args): |
3607 """Patches in a code review.""" | 3814 """Patches in a code review.""" |
3608 parser.add_option('-b', dest='newbranch', | 3815 parser.add_option('-b', dest='newbranch', |
3609 help='create a new branch off trunk for the patch') | 3816 help='create a new branch off trunk for the patch') |
3610 parser.add_option('-f', '--force', action='store_true', | 3817 parser.add_option('-f', '--force', action='store_true', |
3611 help='with -b, clobber any existing branch') | 3818 help='with -b, clobber any existing branch') |
3612 parser.add_option('-d', '--directory', action='store', metavar='DIR', | 3819 parser.add_option('-d', '--directory', action='store', metavar='DIR', |
3613 help='Change to the directory DIR immediately, ' | 3820 help='Change to the directory DIR immediately, ' |
3614 'before doing anything else.') | 3821 'before doing anything else. Rietveld only.') |
3615 parser.add_option('--reject', action='store_true', | 3822 parser.add_option('--reject', action='store_true', |
3616 help='failed patches spew .rej files rather than ' | 3823 help='failed patches spew .rej files rather than ' |
3617 'attempting a 3-way merge') | 3824 'attempting a 3-way merge. Rietveld only.') |
3618 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit', | 3825 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit', |
3619 help="don't commit after patch applies") | 3826 help='don\'t commit after patch applies. Rietveld only.') |
3620 | 3827 |
3621 group = optparse.OptionGroup(parser, | 3828 |
3622 """Options for continuing work on the current issue uploaded | 3829 group = optparse.OptionGroup( |
3623 from a different clone (e.g. different machine). Must be used independently from | 3830 parser, |
3624 the other options. No issue number should be specified, and the branch must have | 3831 'Options for continuing work on the current issue uploaded from a ' |
3625 an issue number associated with it""") | 3832 'different clone (e.g. different machine). Must be used independently ' |
3626 group.add_option('--reapply', action='store_true', | 3833 'from the other options. No issue number should be specified, and the ' |
3627 dest='reapply', | 3834 'branch must have an issue number associated with it') |
3628 help="""Reset the branch and reapply the issue. | 3835 group.add_option('--reapply', action='store_true', dest='reapply', |
3629 CAUTION: This will undo any local changes in this branch""") | 3836 help='Reset the branch and reapply the issue.\n' |
| 3837 'CAUTION: This will undo any local changes in this ' |
| 3838 'branch') |
3630 | 3839 |
3631 group.add_option('--pull', action='store_true', dest='pull', | 3840 group.add_option('--pull', action='store_true', dest='pull', |
3632 help="Performs a pull before reapplying.") | 3841 help='Performs a pull before reapplying.') |
3633 parser.add_option_group(group) | 3842 parser.add_option_group(group) |
3634 | 3843 |
3635 auth.add_auth_options(parser) | 3844 auth.add_auth_options(parser) |
3636 (options, args) = parser.parse_args(args) | 3845 (options, args) = parser.parse_args(args) |
3637 auth_config = auth.extract_auth_config_from_options(options) | 3846 auth_config = auth.extract_auth_config_from_options(options) |
3638 | 3847 |
| 3848 cl = Changelist(auth_config=auth_config) |
| 3849 |
3639 issue_arg = None | 3850 issue_arg = None |
3640 if options.reapply : | 3851 if options.reapply : |
3641 if len(args) > 0: | 3852 if len(args) > 0: |
3642 parser.error("--reapply implies no additional arguments.") | 3853 parser.error('--reapply implies no additional arguments.') |
3643 | 3854 |
3644 cl = Changelist() | |
3645 issue_arg = cl.GetIssue() | 3855 issue_arg = cl.GetIssue() |
3646 upstream = cl.GetUpstreamBranch() | 3856 upstream = cl.GetUpstreamBranch() |
3647 if upstream == None: | 3857 if upstream == None: |
3648 parser.error("No upstream branch specified. Cannot reset branch") | 3858 parser.error('No upstream branch specified. Cannot reset branch') |
3649 | 3859 |
3650 RunGit(['reset', '--hard', upstream]) | 3860 RunGit(['reset', '--hard', upstream]) |
3651 if options.pull: | 3861 if options.pull: |
3652 RunGit(['pull']) | 3862 RunGit(['pull']) |
3653 else: | 3863 else: |
3654 if len(args) != 1: | 3864 if len(args) != 1: |
3655 parser.error("Must specify issue number") | 3865 parser.error('Must specify issue number or url') |
| 3866 issue_arg = args[0] |
3656 | 3867 |
3657 issue_arg = ParseIssueNum(args[0]) | 3868 if not issue_arg: |
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: | |
3662 parser.print_help() | 3869 parser.print_help() |
3663 return 1 | 3870 return 1 |
3664 | 3871 |
| 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 |
3665 # We don't want uncommitted changes mixed up with the patch. | 3880 # We don't want uncommitted changes mixed up with the patch. |
3666 if git_common.is_dirty_git_tree('patch'): | 3881 if git_common.is_dirty_git_tree('patch'): |
3667 return 1 | 3882 return 1 |
3668 | 3883 |
3669 # TODO(maruel): Use apply_issue.py | |
3670 # TODO(ukai): use gerrit-cherry-pick for gerrit repository? | |
3671 | |
3672 if options.newbranch: | 3884 if options.newbranch: |
3673 if options.reapply: | 3885 if options.reapply: |
3674 parser.error("--reapply excludes any option other than --pull") | 3886 parser.error("--reapply excludes any option other than --pull") |
3675 if options.force: | 3887 if options.force: |
3676 RunGit(['branch', '-D', options.newbranch], | 3888 RunGit(['branch', '-D', options.newbranch], |
3677 stderr=subprocess2.PIPE, error_ok=True) | 3889 stderr=subprocess2.PIPE, error_ok=True) |
3678 RunGit(['checkout', '-b', options.newbranch, | 3890 RunGit(['checkout', '-b', options.newbranch, |
3679 Changelist().GetUpstreamBranch()]) | 3891 Changelist().GetUpstreamBranch()]) |
3680 | 3892 |
3681 return PatchIssue(issue_arg, options.reject, options.nocommit, | 3893 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit, |
3682 options.directory, auth_config) | 3894 options.directory) |
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 | |
3759 | 3895 |
3760 | 3896 |
3761 def CMDrebase(parser, args): | 3897 def CMDrebase(parser, args): |
3762 """Rebases current branch on top of svn repo.""" | 3898 """Rebases current branch on top of svn repo.""" |
3763 # Provide a wrapper for git svn rebase to help avoid accidental | 3899 # Provide a wrapper for git svn rebase to help avoid accidental |
3764 # git svn dcommit. | 3900 # git svn dcommit. |
3765 # It's the only command that doesn't use parser at all since we just defer | 3901 # It's the only command that doesn't use parser at all since we just defer |
3766 # execution to git-svn. | 3902 # execution to git-svn. |
3767 | 3903 |
3768 return RunGitWithCode(['svn', 'rebase'] + args)[1] | 3904 return RunGitWithCode(['svn', 'rebase'] + args)[1] |
(...skipping 381 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
4150 | 4286 |
4151 def CMDdiff(parser, args): | 4287 def CMDdiff(parser, args): |
4152 """Shows differences between local tree and last upload.""" | 4288 """Shows differences between local tree and last upload.""" |
4153 auth.add_auth_options(parser) | 4289 auth.add_auth_options(parser) |
4154 options, args = parser.parse_args(args) | 4290 options, args = parser.parse_args(args) |
4155 auth_config = auth.extract_auth_config_from_options(options) | 4291 auth_config = auth.extract_auth_config_from_options(options) |
4156 if args: | 4292 if args: |
4157 parser.error('Unrecognized args: %s' % ' '.join(args)) | 4293 parser.error('Unrecognized args: %s' % ' '.join(args)) |
4158 | 4294 |
4159 # Uncommitted (staged and unstaged) changes will be destroyed by | 4295 # Uncommitted (staged and unstaged) changes will be destroyed by |
4160 # "git reset --hard" if there are merging conflicts in PatchIssue(). | 4296 # "git reset --hard" if there are merging conflicts in CMDPatchIssue(). |
4161 # Staged changes would be committed along with the patch from last | 4297 # Staged changes would be committed along with the patch from last |
4162 # upload, hence counted toward the "last upload" side in the final | 4298 # upload, hence counted toward the "last upload" side in the final |
4163 # diff output, and this is not what we want. | 4299 # diff output, and this is not what we want. |
4164 if git_common.is_dirty_git_tree('diff'): | 4300 if git_common.is_dirty_git_tree('diff'): |
4165 return 1 | 4301 return 1 |
4166 | 4302 |
4167 cl = Changelist(auth_config=auth_config) | 4303 cl = Changelist(auth_config=auth_config) |
4168 issue = cl.GetIssue() | 4304 issue = cl.GetIssue() |
4169 branch = cl.GetBranch() | 4305 branch = cl.GetBranch() |
4170 if not issue: | 4306 if not issue: |
4171 DieWithError('No issue found for current branch (%s)' % branch) | 4307 DieWithError('No issue found for current branch (%s)' % branch) |
4172 TMP_BRANCH = 'git-cl-diff' | 4308 TMP_BRANCH = 'git-cl-diff' |
4173 base_branch = cl.GetCommonAncestorWithUpstream() | 4309 base_branch = cl.GetCommonAncestorWithUpstream() |
4174 | 4310 |
4175 # Create a new branch based on the merge-base | 4311 # Create a new branch based on the merge-base |
4176 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch]) | 4312 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch]) |
4177 try: | 4313 try: |
4178 # Patch in the latest changes from rietveld. | 4314 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None) |
4179 rtn = PatchIssue(issue, False, False, None, auth_config) | |
4180 if rtn != 0: | 4315 if rtn != 0: |
4181 RunGit(['reset', '--hard']) | 4316 RunGit(['reset', '--hard']) |
4182 return rtn | 4317 return rtn |
4183 | 4318 |
4184 # Switch back to starting branch and diff against the temporary | 4319 # Switch back to starting branch and diff against the temporary |
4185 # branch containing the latest rietveld patch. | 4320 # branch containing the latest rietveld patch. |
4186 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--']) | 4321 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--']) |
4187 finally: | 4322 finally: |
4188 RunGit(['checkout', '-q', branch]) | 4323 RunGit(['checkout', '-q', branch]) |
4189 RunGit(['branch', '-D', TMP_BRANCH]) | 4324 RunGit(['branch', '-D', TMP_BRANCH]) |
(...skipping 187 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
4377 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir) | 4512 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir) |
4378 if opts.diff: | 4513 if opts.diff: |
4379 sys.stdout.write(stdout) | 4514 sys.stdout.write(stdout) |
4380 | 4515 |
4381 return return_value | 4516 return return_value |
4382 | 4517 |
4383 | 4518 |
4384 @subcommand.usage('<codereview url or issue id>') | 4519 @subcommand.usage('<codereview url or issue id>') |
4385 def CMDcheckout(parser, args): | 4520 def CMDcheckout(parser, args): |
4386 """Checks out a branch associated with a given Rietveld issue.""" | 4521 """Checks out a branch associated with a given Rietveld issue.""" |
| 4522 # TODO(tandrii): consider adding this for Gerrit? |
4387 _, args = parser.parse_args(args) | 4523 _, args = parser.parse_args(args) |
4388 | 4524 |
4389 if len(args) != 1: | 4525 if len(args) != 1: |
4390 parser.print_help() | 4526 parser.print_help() |
4391 return 1 | 4527 return 1 |
4392 | 4528 |
4393 target_issue = ParseIssueNum(args[0]) | 4529 issue_arg = ParseIssueNumberArgument(args[0]) |
4394 if target_issue == None: | 4530 if issue_arg.valid: |
4395 parser.print_help() | 4531 parser.print_help() |
4396 return 1 | 4532 return 1 |
| 4533 target_issue = issue_arg.issue |
4397 | 4534 |
4398 key_and_issues = [x.split() for x in RunGit( | 4535 key_and_issues = [x.split() for x in RunGit( |
4399 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue']) | 4536 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue']) |
4400 .splitlines()] | 4537 .splitlines()] |
4401 branches = [] | 4538 branches = [] |
4402 for key, issue in key_and_issues: | 4539 for key, issue in key_and_issues: |
4403 if issue == target_issue: | 4540 if issue == target_issue: |
4404 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key)) | 4541 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key)) |
4405 | 4542 |
4406 if len(branches) == 0: | 4543 if len(branches) == 0: |
(...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
4477 if __name__ == '__main__': | 4614 if __name__ == '__main__': |
4478 # These affect sys.stdout so do it outside of main() to simplify mocks in | 4615 # These affect sys.stdout so do it outside of main() to simplify mocks in |
4479 # unit testing. | 4616 # unit testing. |
4480 fix_encoding.fix_encoding() | 4617 fix_encoding.fix_encoding() |
4481 colorama.init(wrap="TERM" not in os.environ) | 4618 colorama.init(wrap="TERM" not in os.environ) |
4482 try: | 4619 try: |
4483 sys.exit(main(sys.argv[1:])) | 4620 sys.exit(main(sys.argv[1:])) |
4484 except KeyboardInterrupt: | 4621 except KeyboardInterrupt: |
4485 sys.stderr.write('interrupted\n') | 4622 sys.stderr.write('interrupted\n') |
4486 sys.exit(1) | 4623 sys.exit(1) |
OLD | NEW |