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

Side by Side Diff: git_cl.py

Issue 200023004: Add a --squash option to Gerrit uploads. (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/depot_tools.git@master
Patch Set: tests Created 5 years, 10 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.""" 8 """A git-command for integrating reviews on Rietveld."""
9 9
10 from distutils.version import LooseVersion 10 from distutils.version import LooseVersion
11 import base64 11 import base64
12 import glob 12 import glob
13 import json 13 import json
14 import logging 14 import logging
15 import optparse 15 import optparse
16 import os 16 import os
17 import Queue 17 import Queue
18 import re 18 import re
19 import stat 19 import stat
20 import sys 20 import sys
21 import tempfile
21 import textwrap 22 import textwrap
22 import threading 23 import threading
23 import urllib2 24 import urllib2
24 import urlparse 25 import urlparse
25 import webbrowser 26 import webbrowser
26 import zlib 27 import zlib
27 28
28 try: 29 try:
29 import readline # pylint: disable=F0401,W0611 30 import readline # pylint: disable=F0401,W0611
30 except ImportError: 31 except ImportError:
(...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after
104 stderr = sys.stderr 105 stderr = sys.stderr
105 out, code = subprocess2.communicate(['git'] + args, 106 out, code = subprocess2.communicate(['git'] + args,
106 env=GetNoGitPagerEnv(), 107 env=GetNoGitPagerEnv(),
107 stdout=subprocess2.PIPE, 108 stdout=subprocess2.PIPE,
108 stderr=stderr) 109 stderr=stderr)
109 return code, out[0] 110 return code, out[0]
110 except ValueError: 111 except ValueError:
111 # When the subprocess fails, it returns None. That triggers a ValueError 112 # When the subprocess fails, it returns None. That triggers a ValueError
112 # when trying to unpack the return value into (out, code). 113 # when trying to unpack the return value into (out, code).
113 return 1, '' 114 return 1, ''
114 115
M-A Ruel 2015/02/13 16:19:27 2 lines
Bernhard Bauer 2015/02/13 17:08:11 Done.
116 def RunGitSilent(args):
117 """Returns stdout, supresses stderr and ignores the return code."""
M-A Ruel 2015/02/13 16:19:27 suppresses
Bernhard Bauer 2015/02/13 17:08:11 Done.
118 return RunGitWithCode(args, suppress_stderr=True)[1]
119
115 120
116 def IsGitVersionAtLeast(min_version): 121 def IsGitVersionAtLeast(min_version):
117 prefix = 'git version ' 122 prefix = 'git version '
118 version = RunGit(['--version']).strip() 123 version = RunGit(['--version']).strip()
119 return (version.startswith(prefix) and 124 return (version.startswith(prefix) and
120 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version)) 125 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
121 126
122 127
123 def ask_for_data(prompt): 128 def ask_for_data(prompt):
124 try: 129 try:
(...skipping 1501 matching lines...) Expand 10 before | Expand all | Expand 10 after
1626 if CHANGE_ID in new_log_desc: 1631 if CHANGE_ID in new_log_desc:
1627 print 'git-cl: Added Change-Id to commit message.' 1632 print 'git-cl: Added Change-Id to commit message.'
1628 else: 1633 else:
1629 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.' 1634 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1630 1635
1631 1636
1632 def GerritUpload(options, args, cl, change): 1637 def GerritUpload(options, args, cl, change):
1633 """upload the current branch to gerrit.""" 1638 """upload the current branch to gerrit."""
1634 # We assume the remote called "origin" is the one we want. 1639 # We assume the remote called "origin" is the one we want.
1635 # It is probably not worthwhile to support different workflows. 1640 # It is probably not worthwhile to support different workflows.
1636 remote = 'origin' 1641 gerrit_remote = 'origin'
1637 branch = 'master' 1642 branch = 'master'
1638 if options.target_branch: 1643 if options.target_branch:
1639 branch = options.target_branch 1644 branch = options.target_branch
1640 1645
1641 change_desc = ChangeDescription( 1646 change_desc = ChangeDescription(
1642 options.message or CreateDescriptionFromLog(args)) 1647 options.message or CreateDescriptionFromLog(args))
1643 if not change_desc.description: 1648 if not change_desc.description:
1644 print "Description is empty; aborting." 1649 print "Description is empty; aborting."
1645 return 1 1650 return 1
1646 if CHANGE_ID not in change_desc.description:
1647 AddChangeIdToCommitMessage(options, args)
1648 1651
1649 commits = RunGit(['rev-list', '%s/%s..' % (remote, branch)]).splitlines() 1652 if options.squash:
1653 # Try to get the message from a previous upload.
1654 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
1655 message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
1656 if not message:
1657 if not options.force:
1658 change_desc.prompt()
1659
1660 if CHANGE_ID not in change_desc.description:
1661 # Run the commit-msg hook without modifying the head commit by writing
1662 # the commit message to a temporary file and running the hook over it,
1663 # then reading the file back in.
1664 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
1665 'commit-msg')
1666 file_handle, msg_file = tempfile.mkstemp(text=True, prefix='commit_msg')
M-A Ruel 2015/02/13 16:19:27 You have to os.close() it otherwise it'll fail on
Bernhard Bauer 2015/02/13 17:08:10 Done.
1667 with os.fdopen(file_handle, 'w') as fileobj:
1668 fileobj.write(change_desc.description)
1669 try:
1670 RunCommand([commit_msg_hook, msg_file])
1671 change_desc.set_description(gclient_utils.FileRead(msg_file))
1672 finally:
1673 os.remove(msg_file)
1674
1675 if not change_desc.description:
1676 print "Description is empty; aborting."
1677 return 1
1678
1679 message = change_desc.description
1680
1681 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1682 if remote is '.':
1683 # If our upstream branch is local, we base our squashed commit on its
1684 # squashed version.
1685 parent = ('refs/heads/git_cl_uploads/' +
1686 scm.GIT.ShortBranchName(upstream_branch))
1687
1688 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
1689 # will create additional CLs when uploading.
1690 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
1691 RunGitSilent(['rev-parse', parent + ':'])):
1692 print 'Upload upstream branch ' + upstream_branch + ' first.'
1693 return 1
1694 else:
1695 parent = cl.GetCommonAncestorWithUpstream()
1696
1697 tree = RunGit(['rev-parse', 'HEAD:']).strip()
1698 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
1699 '-m', message]).strip()
1700 else:
1701 if CHANGE_ID not in change_desc.description:
1702 AddChangeIdToCommitMessage(options, args)
1703 ref_to_push = 'HEAD'
1704 parent = '%s/%s' % (gerrit_remote, branch)
1705
1706 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
1707 ref_to_push)]).splitlines()
1650 if len(commits) > 1: 1708 if len(commits) > 1:
1651 print('WARNING: This will upload %d commits. Run the following command ' 1709 print('WARNING: This will upload %d commits. Run the following command '
1652 'to see which commits will be uploaded: ' % len(commits)) 1710 'to see which commits will be uploaded: ' % len(commits))
1653 print('git log %s/%s..' % (remote, branch)) 1711 print('git log %s..%s' % (parent, ref_to_push))
1654 print('You can also use `git squash-branch` to squash these into a single' 1712 print('You can also use `git squash-branch` to squash these into a single '
1655 'commit.') 1713 'commit.')
1656 ask_for_data('About to upload; enter to confirm.') 1714 ask_for_data('About to upload; enter to confirm.')
1657 1715
1658 if options.reviewers or options.tbr_owners: 1716 if options.reviewers or options.tbr_owners:
1659 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change) 1717 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
1660 1718
1661 receive_options = [] 1719 receive_options = []
1662 cc = cl.GetCCList().split(',') 1720 cc = cl.GetCCList().split(',')
1663 if options.cc: 1721 if options.cc:
1664 cc.extend(options.cc) 1722 cc.extend(options.cc)
1665 cc = filter(None, cc) 1723 cc = filter(None, cc)
1666 if cc: 1724 if cc:
1667 receive_options += ['--cc=' + email for email in cc] 1725 receive_options += ['--cc=' + email for email in cc]
1668 if change_desc.get_reviewers(): 1726 if change_desc.get_reviewers():
1669 receive_options.extend( 1727 receive_options.extend(
1670 '--reviewer=' + email for email in change_desc.get_reviewers()) 1728 '--reviewer=' + email for email in change_desc.get_reviewers())
1671 1729
1672 git_command = ['push'] 1730 git_command = ['push']
1673 if receive_options: 1731 if receive_options:
1674 git_command.append('--receive-pack=git receive-pack %s' % 1732 git_command.append('--receive-pack=git receive-pack %s' %
1675 ' '.join(receive_options)) 1733 ' '.join(receive_options))
1676 git_command += [remote, 'HEAD:refs/for/' + branch] 1734 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
1677 RunGit(git_command) 1735 RunGit(git_command)
1736
1737 if options.squash:
1738 head = RunGit(['rev-parse', 'HEAD']).strip()
1739 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
1740
1678 # TODO(ukai): parse Change-Id: and set issue number? 1741 # TODO(ukai): parse Change-Id: and set issue number?
1679 return 0 1742 return 0
1680 1743
1681 1744
1682 def GetTargetRef(remote, remote_branch, target_branch, pending_prefix): 1745 def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
1683 """Computes the remote branch ref to use for the CL. 1746 """Computes the remote branch ref to use for the CL.
1684 1747
1685 Args: 1748 Args:
1686 remote (str): The git remote for the CL. 1749 remote (str): The git remote for the CL.
1687 remote_branch (str): The git remote branch for the CL. 1750 remote_branch (str): The git remote branch for the CL.
1688 target_branch (str): The target branch specified by the user. 1751 target_branch (str): The target branch specified by the user.
1689 pending_prefix (str): The pending prefix from the settings. 1752 pending_prefix (str): The pending prefix from the settings.
1690 """ 1753 """
1691 if not (remote and remote_branch): 1754 if not (remote and remote_branch):
1692 return None 1755 return None
1693 1756
1694 if target_branch: 1757 if target_branch:
1695 # Cannonicalize branch references to the equivalent local full symbolic 1758 # Cannonicalize branch references to the equivalent local full symbolic
1696 # refs, which are then translated into the remote full symbolic refs 1759 # refs, which are then translated into the remote full symbolic refs
1697 # below. 1760 # below.
1698 if '/' not in target_branch: 1761 if '/' not in target_branch:
1699 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch) 1762 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
1700 else: 1763 else:
1701 prefix_replacements = ( 1764 prefix_replacements = (
1702 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'), 1765 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
1703 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote), 1766 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
1704 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote), 1767 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
1705 ) 1768 )
1706 match = None 1769 match = None
1707 for regex, replacement in prefix_replacements: 1770 for regex, replacement in prefix_replacements:
1708 match = re.search(regex, target_branch) 1771 match = re.search(regex, target_branch)
1709 if match: 1772 if match:
1710 remote_branch = target_branch.replace(match.group(0), replacement) 1773 remote_branch = target_branch.replace(match.group(0), replacement)
1711 break 1774 break
1712 if not match: 1775 if not match:
1713 # This is a branch path but not one we recognize; use as-is. 1776 # This is a branch path but not one we recognize; use as-is.
1714 remote_branch = target_branch 1777 remote_branch = target_branch
1715 elif (not remote_branch.startswith('refs/remotes/branch-heads') and 1778 elif (not remote_branch.startswith('refs/remotes/branch-heads') and
1716 not remote_branch.startswith('refs/remotes/%s/refs' % remote)): 1779 not remote_branch.startswith('refs/remotes/%s/refs' % remote)):
1717 # Default to master for refs that are not branches. 1780 # Default to master for refs that are not branches.
1718 remote_branch = 'refs/remotes/%s/master' % remote 1781 remote_branch = 'refs/remotes/%s/master' % remote
1719 1782
1720 # Create the true path to the remote branch. 1783 # Create the true path to the remote branch.
1721 # Does the following translation: 1784 # Does the following translation:
1722 # * refs/remotes/origin/refs/diff/test -> refs/diff/test 1785 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
1723 # * refs/remotes/origin/master -> refs/heads/master 1786 # * refs/remotes/origin/master -> refs/heads/master
1724 # * refs/remotes/branch-heads/test -> refs/branch-heads/test 1787 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
1725 if remote_branch.startswith('refs/remotes/%s/refs/' % remote): 1788 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
1726 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '') 1789 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
1727 elif remote_branch.startswith('refs/remotes/%s/' % remote): 1790 elif remote_branch.startswith('refs/remotes/%s/' % remote):
1728 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, 1791 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
1729 'refs/heads/') 1792 'refs/heads/')
(...skipping 159 matching lines...) Expand 10 before | Expand all | Expand 10 after
1889 help="Emulate Subversion's auto properties feature.") 1952 help="Emulate Subversion's auto properties feature.")
1890 parser.add_option('-c', '--use-commit-queue', action='store_true', 1953 parser.add_option('-c', '--use-commit-queue', action='store_true',
1891 help='tell the commit queue to commit this patchset') 1954 help='tell the commit queue to commit this patchset')
1892 parser.add_option('--private', action='store_true', 1955 parser.add_option('--private', action='store_true',
1893 help='set the review private (rietveld only)') 1956 help='set the review private (rietveld only)')
1894 parser.add_option('--target_branch', 1957 parser.add_option('--target_branch',
1895 '--target-branch', 1958 '--target-branch',
1896 metavar='TARGET', 1959 metavar='TARGET',
1897 help='Apply CL to remote ref TARGET. ' + 1960 help='Apply CL to remote ref TARGET. ' +
1898 'Default: remote branch head, or master') 1961 'Default: remote branch head, or master')
1962 parser.add_option('--squash', action='store_true',
1963 help='Squash multiple commits into one (Gerrit only)')
1899 parser.add_option('--email', default=None, 1964 parser.add_option('--email', default=None,
1900 help='email address to use to connect to Rietveld') 1965 help='email address to use to connect to Rietveld')
1901 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true', 1966 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
1902 help='add a set of OWNERS to TBR') 1967 help='add a set of OWNERS to TBR')
1903 1968
1904 add_git_similarity(parser) 1969 add_git_similarity(parser)
1905 (options, args) = parser.parse_args(args) 1970 (options, args) = parser.parse_args(args)
1906 1971
1907 if is_dirty_git_tree('upload'): 1972 if is_dirty_git_tree('upload'):
1908 return 1 1973 return 1
(...skipping 1065 matching lines...) Expand 10 before | Expand all | Expand 10 after
2974 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith ' 3039 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2975 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e))) 3040 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
2976 3041
2977 3042
2978 if __name__ == '__main__': 3043 if __name__ == '__main__':
2979 # These affect sys.stdout so do it outside of main() to simplify mocks in 3044 # These affect sys.stdout so do it outside of main() to simplify mocks in
2980 # unit testing. 3045 # unit testing.
2981 fix_encoding.fix_encoding() 3046 fix_encoding.fix_encoding()
2982 colorama.init() 3047 colorama.init()
2983 sys.exit(main(sys.argv[1:])) 3048 sys.exit(main(sys.argv[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