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.""" | 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 Loading... | |
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 Loading... | |
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 Loading... | |
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 Loading... | |
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:])) |
OLD | NEW |