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

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