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

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