mirror of
https://chromium.googlesource.com/chromium/tools/depot_tools.git
synced 2026-01-11 18:51:29 +00:00
git-squash-branch: handle empty squashes
Error out of the current tree is dirty (previously the dirty content would be incorporated silently into the newly squashed branch!). Review URL: https://codereview.chromium.org/1064933004 git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@294744 0039d316-1c4b-4281-b951-d872f2087c98
This commit is contained in:
26
git_cl.py
26
git_cl.py
@@ -201,20 +201,6 @@ def add_git_similarity(parser):
|
|||||||
parser.parse_args = Parse
|
parser.parse_args = Parse
|
||||||
|
|
||||||
|
|
||||||
def is_dirty_git_tree(cmd):
|
|
||||||
# Make sure index is up-to-date before running diff-index.
|
|
||||||
RunGit(['update-index', '--refresh', '-q'], error_ok=True)
|
|
||||||
dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
|
|
||||||
if dirty:
|
|
||||||
print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
|
|
||||||
print 'Uncommitted files: (git diff-index --name-status HEAD)'
|
|
||||||
print dirty[:4096]
|
|
||||||
if len(dirty) > 4096:
|
|
||||||
print '... (run "git diff-index --name-status HEAD" to see full output).'
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
|
def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
|
||||||
"""Return the corresponding git ref if |base_url| together with |glob_spec|
|
"""Return the corresponding git ref if |base_url| together with |glob_spec|
|
||||||
matches the full |url|.
|
matches the full |url|.
|
||||||
@@ -1655,7 +1641,7 @@ def CMDpresubmit(parser, args):
|
|||||||
help='Run checks even if tree is dirty')
|
help='Run checks even if tree is dirty')
|
||||||
(options, args) = parser.parse_args(args)
|
(options, args) = parser.parse_args(args)
|
||||||
|
|
||||||
if not options.force and is_dirty_git_tree('presubmit'):
|
if not options.force and git_common.is_dirty_git_tree('presubmit'):
|
||||||
print 'use --force to check even if tree is dirty.'
|
print 'use --force to check even if tree is dirty.'
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
@@ -2032,7 +2018,7 @@ def CMDupload(parser, args):
|
|||||||
add_git_similarity(parser)
|
add_git_similarity(parser)
|
||||||
(options, args) = parser.parse_args(args)
|
(options, args) = parser.parse_args(args)
|
||||||
|
|
||||||
if is_dirty_git_tree('upload'):
|
if git_common.is_dirty_git_tree('upload'):
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
options.reviewers = cleanup_list(options.reviewers)
|
options.reviewers = cleanup_list(options.reviewers)
|
||||||
@@ -2162,7 +2148,7 @@ def SendUpstream(parser, args, cmd):
|
|||||||
base_branch = args[0]
|
base_branch = args[0]
|
||||||
base_has_submodules = IsSubmoduleMergeCommit(base_branch)
|
base_has_submodules = IsSubmoduleMergeCommit(base_branch)
|
||||||
|
|
||||||
if is_dirty_git_tree(cmd):
|
if git_common.is_dirty_git_tree(cmd):
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# This rev-list syntax means "show all commits not in my branch that
|
# This rev-list syntax means "show all commits not in my branch that
|
||||||
@@ -2538,7 +2524,7 @@ def CMDpatch(parser, args):
|
|||||||
issue_arg = args[0]
|
issue_arg = args[0]
|
||||||
|
|
||||||
# We don't want uncommitted changes mixed up with the patch.
|
# We don't want uncommitted changes mixed up with the patch.
|
||||||
if is_dirty_git_tree('patch'):
|
if git_common.is_dirty_git_tree('patch'):
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# TODO(maruel): Use apply_issue.py
|
# TODO(maruel): Use apply_issue.py
|
||||||
@@ -2558,7 +2544,7 @@ def CMDpatch(parser, args):
|
|||||||
def PatchIssue(issue_arg, reject, nocommit, directory):
|
def PatchIssue(issue_arg, reject, nocommit, directory):
|
||||||
# There's a "reset --hard" when failing to apply the patch. In order
|
# There's a "reset --hard" when failing to apply the patch. In order
|
||||||
# not to destroy users' data, make sure the tree is not dirty here.
|
# not to destroy users' data, make sure the tree is not dirty here.
|
||||||
assert(not is_dirty_git_tree('apply'))
|
assert(not git_common.is_dirty_git_tree('apply'))
|
||||||
|
|
||||||
if type(issue_arg) is int or issue_arg.isdigit():
|
if type(issue_arg) is int or issue_arg.isdigit():
|
||||||
# Input is an issue id. Figure out the URL.
|
# Input is an issue id. Figure out the URL.
|
||||||
@@ -2921,7 +2907,7 @@ def CMDdiff(parser, args):
|
|||||||
# Staged changes would be committed along with the patch from last
|
# Staged changes would be committed along with the patch from last
|
||||||
# upload, hence counted toward the "last upload" side in the final
|
# upload, hence counted toward the "last upload" side in the final
|
||||||
# diff output, and this is not what we want.
|
# diff output, and this is not what we want.
|
||||||
if is_dirty_git_tree('diff'):
|
if git_common.is_dirty_git_tree('diff'):
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
cl = Changelist()
|
cl = Changelist()
|
||||||
|
|||||||
@@ -602,6 +602,24 @@ def set_config(option, value, scope='local'):
|
|||||||
run('config', '--' + scope, option, value)
|
run('config', '--' + scope, option, value)
|
||||||
|
|
||||||
|
|
||||||
|
def get_dirty_files():
|
||||||
|
# Make sure index is up-to-date before running diff-index.
|
||||||
|
run_with_retcode('update-index', '--refresh', '-q')
|
||||||
|
return run('diff-index', '--name-status', 'HEAD')
|
||||||
|
|
||||||
|
|
||||||
|
def is_dirty_git_tree(cmd):
|
||||||
|
dirty = get_dirty_files()
|
||||||
|
if dirty:
|
||||||
|
print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
|
||||||
|
print 'Uncommitted files: (git diff-index --name-status HEAD)'
|
||||||
|
print dirty[:4096]
|
||||||
|
if len(dirty) > 4096: # pragma: no cover
|
||||||
|
print '... (run "git diff-index --name-status HEAD" to see full output).'
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def squash_current_branch(header=None, merge_base=None):
|
def squash_current_branch(header=None, merge_base=None):
|
||||||
header = header or 'git squash commit.'
|
header = header or 'git squash commit.'
|
||||||
merge_base = merge_base or get_or_create_merge_base(current_branch())
|
merge_base = merge_base or get_or_create_merge_base(current_branch())
|
||||||
@@ -610,7 +628,14 @@ def squash_current_branch(header=None, merge_base=None):
|
|||||||
log_msg += '\n'
|
log_msg += '\n'
|
||||||
log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
|
log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
|
||||||
run('reset', '--soft', merge_base)
|
run('reset', '--soft', merge_base)
|
||||||
|
|
||||||
|
if not get_dirty_files():
|
||||||
|
# Sometimes the squash can result in the same tree, meaning that there is
|
||||||
|
# nothing to commit at this point.
|
||||||
|
print 'Nothing to commit; squashed branch is empty'
|
||||||
|
return False
|
||||||
run('commit', '-a', '-F', '-', indata=log_msg)
|
run('commit', '-a', '-F', '-', indata=log_msg)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def tags(*args):
|
def tags(*args):
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from git_common import squash_current_branch
|
import git_common
|
||||||
|
|
||||||
def main(args):
|
def main(args):
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
@@ -14,7 +14,9 @@ def main(args):
|
|||||||
'-m', '--message', metavar='<msg>', default='git squash commit.',
|
'-m', '--message', metavar='<msg>', default='git squash commit.',
|
||||||
help='Use the given <msg> as the first line of the commit message.')
|
help='Use the given <msg> as the first line of the commit message.')
|
||||||
opts = parser.parse_args(args)
|
opts = parser.parse_args(args)
|
||||||
squash_current_branch(opts.message)
|
if git_common.is_dirty_git_tree('squash-branch'):
|
||||||
|
return 1
|
||||||
|
git_common.squash_current_branch(opts.message)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -349,7 +349,6 @@ class GitRepo(object):
|
|||||||
env['GIT_%s' % singleton] = str(val)
|
env['GIT_%s' % singleton] = str(val)
|
||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
||||||
def git(self, *args, **kwargs):
|
def git(self, *args, **kwargs):
|
||||||
"""Runs a git command specified by |args| in this repo."""
|
"""Runs a git command specified by |args| in this repo."""
|
||||||
assert self.repo_path is not None
|
assert self.repo_path is not None
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class TestGitCl(TestCase):
|
|||||||
self.mock(subprocess2, 'check_call', self._mocked_call)
|
self.mock(subprocess2, 'check_call', self._mocked_call)
|
||||||
self.mock(subprocess2, 'check_output', self._mocked_call)
|
self.mock(subprocess2, 'check_output', self._mocked_call)
|
||||||
self.mock(subprocess2, 'communicate', self._mocked_call)
|
self.mock(subprocess2, 'communicate', self._mocked_call)
|
||||||
self.mock(subprocess2, 'Popen', self._mocked_call)
|
self.mock(git_common, 'is_dirty_git_tree', lambda x: False)
|
||||||
self.mock(git_common, 'get_or_create_merge_base',
|
self.mock(git_common, 'get_or_create_merge_base',
|
||||||
lambda *a: (
|
lambda *a: (
|
||||||
self._mocked_call(['get_or_create_merge_base']+list(a))))
|
self._mocked_call(['get_or_create_merge_base']+list(a))))
|
||||||
@@ -156,8 +156,6 @@ class TestGitCl(TestCase):
|
|||||||
similarity_call,
|
similarity_call,
|
||||||
((['git', 'symbolic-ref', 'HEAD'],), 'master'),
|
((['git', 'symbolic-ref', 'HEAD'],), 'master'),
|
||||||
find_copies_call,
|
find_copies_call,
|
||||||
((['git', 'update-index', '--refresh', '-q'],), ''),
|
|
||||||
((['git', 'diff-index', '--name-status', 'HEAD'],), ''),
|
|
||||||
((['git', 'symbolic-ref', 'HEAD'],), 'master'),
|
((['git', 'symbolic-ref', 'HEAD'],), 'master'),
|
||||||
((['git', 'config', 'branch.master.merge'],), 'master'),
|
((['git', 'config', 'branch.master.merge'],), 'master'),
|
||||||
((['git', 'config', 'branch.master.remote'],), 'origin'),
|
((['git', 'config', 'branch.master.remote'],), 'origin'),
|
||||||
@@ -280,8 +278,6 @@ class TestGitCl(TestCase):
|
|||||||
((['git', 'rev-list', '--merges',
|
((['git', 'rev-list', '--merges',
|
||||||
'--grep=^SVN changes up to revision [0-9]*$',
|
'--grep=^SVN changes up to revision [0-9]*$',
|
||||||
'refs/remotes/origin/master^!'],), ''),
|
'refs/remotes/origin/master^!'],), ''),
|
||||||
((['git', 'update-index', '--refresh', '-q'],), ''),
|
|
||||||
((['git', 'diff-index', '--name-status', 'HEAD'],), ''),
|
|
||||||
((['git', 'rev-list', '^refs/heads/working',
|
((['git', 'rev-list', '^refs/heads/working',
|
||||||
'refs/remotes/origin/master'],),
|
'refs/remotes/origin/master'],),
|
||||||
''),
|
''),
|
||||||
@@ -544,8 +540,6 @@ class TestGitCl(TestCase):
|
|||||||
((['git', 'symbolic-ref', 'HEAD'],), 'master'),
|
((['git', 'symbolic-ref', 'HEAD'],), 'master'),
|
||||||
((['git', 'config', '--int', '--get',
|
((['git', 'config', '--int', '--get',
|
||||||
'branch.master.git-find-copies'],), ''),
|
'branch.master.git-find-copies'],), ''),
|
||||||
((['git', 'update-index', '--refresh', '-q'],), ''),
|
|
||||||
((['git', 'diff-index', '--name-status', 'HEAD'],), ''),
|
|
||||||
((['git', 'symbolic-ref', 'HEAD'],), 'master'),
|
((['git', 'symbolic-ref', 'HEAD'],), 'master'),
|
||||||
((['git', 'config', 'branch.master.merge'],), 'master'),
|
((['git', 'config', 'branch.master.merge'],), 'master'),
|
||||||
((['git', 'config', 'branch.master.remote'],), 'origin'),
|
((['git', 'config', 'branch.master.remote'],), 'origin'),
|
||||||
|
|||||||
@@ -561,10 +561,17 @@ class GitMutableStructuredTest(git_test_utils.GitRepoReadWriteTestBase,
|
|||||||
('root_A', 'root_X'),
|
('root_A', 'root_X'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
def testIsGitTreeDirty(self):
|
||||||
|
self.assertEquals(False, self.repo.run(self.gc.is_dirty_git_tree, 'foo'))
|
||||||
|
self.repo.open('test.file', 'w').write('test data')
|
||||||
|
self.repo.git('add', 'test.file')
|
||||||
|
self.assertEquals(True, self.repo.run(self.gc.is_dirty_git_tree, 'foo'))
|
||||||
|
|
||||||
def testSquashBranch(self):
|
def testSquashBranch(self):
|
||||||
self.repo.git('checkout', 'branch_K')
|
self.repo.git('checkout', 'branch_K')
|
||||||
|
|
||||||
self.repo.run(self.gc.squash_current_branch, 'cool message')
|
self.assertEquals(True, self.repo.run(self.gc.squash_current_branch,
|
||||||
|
'cool message'))
|
||||||
|
|
||||||
lines = ['cool message', '']
|
lines = ['cool message', '']
|
||||||
for l in 'HIJK':
|
for l in 'HIJK':
|
||||||
@@ -580,6 +587,14 @@ class GitMutableStructuredTest(git_test_utils.GitRepoReadWriteTestBase,
|
|||||||
'K'
|
'K'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def testSquashBranchEmpty(self):
|
||||||
|
self.repo.git('checkout', 'branch_K')
|
||||||
|
self.repo.git('checkout', 'branch_G', '.')
|
||||||
|
self.repo.git('commit', '-m', 'revert all changes no branch')
|
||||||
|
# Should return False since the quash would result in an empty commit
|
||||||
|
stdout = self.repo.capture_stdio(self.gc.squash_current_branch)[0]
|
||||||
|
self.assertEquals(stdout, 'Nothing to commit; squashed branch is empty\n')
|
||||||
|
|
||||||
def testRebase(self):
|
def testRebase(self):
|
||||||
self.assertSchema("""
|
self.assertSchema("""
|
||||||
A B C D E F G
|
A B C D E F G
|
||||||
|
|||||||
Reference in New Issue
Block a user