Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(368)

Side by Side Diff: git_cl.py

Issue 1852803002: Reland of Gerrit git cl: implement git cl patch. (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/depot_tools.git@master
Patch Set: Created 4 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | tests/git_cl_test.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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
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
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
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
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
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)
OLDNEW
« no previous file with comments | « no previous file | tests/git_cl_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698