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 |