diff --git a/git_common.py b/git_common.py index e7244790a..03d99ee3f 100644 --- a/git_common.py +++ b/git_common.py @@ -32,8 +32,10 @@ import threading import subprocess2 -ROOT = os.path.abspath(os.path.dirname(__file__)) +from StringIO import StringIO + +ROOT = os.path.abspath(os.path.dirname(__file__)) IS_WIN = sys.platform == 'win32' GIT_EXE = ROOT+'\\git.bat' if IS_WIN else 'git' TEST_MODE = False @@ -392,6 +394,36 @@ def diff(oldrev, newrev, *args): def freeze(): took_action = False + key = 'depot-tools.freeze-size-limit' + MB = 2**20 + limit_mb = get_config_int(key, 100) + untracked_bytes = 0 + + for f, s in status(): + if is_unmerged(s): + die("Cannot freeze unmerged changes!") + if limit_mb > 0: + if s.lstat == '?': + untracked_bytes += os.stat(f).st_size + if untracked_bytes > limit_mb * MB: + die("""\ + You appear to have too much untracked+unignored data in your git + checkout: %.1f / %d MB. + + Run `git status` to see what it is. + + In addition to making many git commands slower, this will prevent + depot_tools from freezing your in-progress changes. + + You should add untracked data that you want to ignore to your repo's + .git/info/excludes + file. See `git help ignore` for the format of this file. + + If this data is indended as part of your commit, you may adjust the + freeze limit by running: + git config %s + Where is an integer threshold in megabytes.""", + untracked_bytes / (MB * 1.0), limit_mb, key) try: run('commit', '--no-verify', '-m', FREEZE + '.indexed') @@ -502,6 +534,13 @@ def is_dormant(branch): return branch_config(branch, 'dormant', 'false') != 'false' +def is_unmerged(stat_value): + return ( + 'U' in (stat_value.lstat, stat_value.rstat) or + ((stat_value.lstat == stat_value.rstat) and stat_value.lstat in 'AD') + ) + + def manual_merge_base(branch, base, parent): set_branch_config(branch, 'base', base) set_branch_config(branch, 'base-upstream', parent) @@ -711,6 +750,45 @@ def is_dirty_git_tree(cmd): return False +def status(): + """Returns a parsed version of git-status. + + Returns a generator of (current_name, (lstat, rstat, src)) pairs where: + * current_name is the name of the file + * lstat is the left status code letter from git-status + * rstat is the left status code letter from git-status + * src is the current name of the file, or the original name of the file + if lstat == 'R' + """ + stat_entry = collections.namedtuple('stat_entry', 'lstat rstat src') + + def tokenizer(stream): + acc = StringIO() + c = None + while c != '': + c = stream.read(1) + if c in (None, '', '\0'): + if acc.len: + yield acc.getvalue() + acc = StringIO() + else: + acc.write(c) + + def parser(tokens): + while True: + # Raises StopIteration if it runs out of tokens. + status_dest = next(tokens) + stat, dest = status_dest[:2], status_dest[3:] + lstat, rstat = stat + if lstat == 'R': + src = next(tokens) + else: + src = dest + yield (dest, stat_entry(lstat, rstat, src)) + + return parser(tokenizer(run_stream('status', '-z', bufsize=-1))) + + def squash_current_branch(header=None, merge_base=None): header = header or 'git squash commit.' merge_base = merge_base or get_or_create_merge_base(current_branch()) diff --git a/man/html/git-freeze.html b/man/html/git-freeze.html index b17a937d7..c29b3ab92 100644 --- a/man/html/git-freeze.html +++ b/man/html/git-freeze.html @@ -773,6 +773,8 @@ exactly the same working state later (by running git thaw).

git add, git mv, git rm, etc.). A commit with the message FREEZE.unindexed will contain all changes which were not in your index at the time you ran git freeze (freshly modified files, new files, etc.).

+

By default git freeze will only freeze up to 100MB of untracked files. See +CONFIGURATION VARIABLES for more details.

@@ -788,7 +790,7 @@ time you ran git freeze (freshly modified files, new files, etc.).

$ git freeze $ git status --short $ git log -n 2 --stat -commit 648c29b68da0142bcad41872339f9732c6ec4470 +commit 07f208f5916bd8ee3affbf8d182c918a8dc1a699 Author: local <local@chromium.org> Date: Thu Apr 10 08:54:56 2014 +0000 @@ -800,7 +802,7 @@ Date: Thu Apr 10 08:54:56 2014 +0000 unstaged_deleted_file | 1 - 4 files changed, 2 insertions(+), 1 deletion(-) -commit a4e49c2896814f52feec3fc06ef0454962ee6b8c +commit 9f803ff1f05dd0c77d31b8958992520e37ed6278 Author: local <local@chromium.org> Date: Thu Apr 10 08:54:56 2014 +0000 @@ -822,6 +824,17 @@ Date: Thu Apr 10 08:54:56 2014 +0000
+

CONFIGURATION VARIABLES

+
+
+

depot-tools.freeze-size-limit

+

This sets the size limit as an integer number of megabytes of untracked files +that git-freeze will be willing to put in suspended animation. A 0 or negative +limit disables the size-limit check entirely. 100 by default.

+
+
+
+

SEE ALSO

@@ -839,7 +852,7 @@ from

diff --git a/man/man1/git-freeze.1 b/man/man1/git-freeze.1 index a43b51668..787d2e53d 100644 --- a/man/man1/git-freeze.1 +++ b/man/man1/git-freeze.1 @@ -2,12 +2,12 @@ .\" Title: git-freeze .\" Author: [FIXME: author] [see http://docbook.sf.net/el/author] .\" Generator: DocBook XSL Stylesheets v1.78.1 -.\" Date: 04/10/2014 +.\" Date: 06/04/2014 .\" Manual: Chromium depot_tools Manual -.\" Source: depot_tools 68b1017 +.\" Source: depot_tools d39bbb5 .\" Language: English .\" -.TH "GIT\-FREEZE" "1" "04/10/2014" "depot_tools 68b1017" "Chromium depot_tools Manual" +.TH "GIT\-FREEZE" "1" "06/04/2014" "depot_tools d39bbb5" "Chromium depot_tools Manual" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- @@ -40,6 +40,8 @@ git-freeze \- Freeze all changes on a branch (indexed and unindexed)\&. git freeze works a lot like git stash, in that it stores the current changes in your working copy and index \fIsomewhere\fR\&. Unlike git stash, git freeze stores those changes on your current branch\&. This effectively allows you to \fIpause\fR development of a branch, work on something else, and then come back to exactly the same working state later (by running git thaw)\&. .sp git freeze will make up to 2 commits on your branch\&. A commit with the message FREEZE\&.indexed will contain all changes which you\(cqve added to your index (like with \fIgit add\fR, \fIgit mv\fR, \fIgit rm\fR, etc\&.)\&. A commit with the message FREEZE\&.unindexed will contain all changes which were not in your index at the time you ran git freeze (freshly modified files, new files, etc\&.)\&. +.sp +By default git freeze will only freeze up to 100MB of untracked files\&. See \fICONFIGURATION VARIABLES\fR for more details\&. .SH "EXAMPLE" .sp @@ -58,7 +60,7 @@ D deleted_file \fB$ git freeze\fR \fB$ git status \-\-short\fR \fB$ git log \-n 2 \-\-stat\fR -commit 85b90eda860a6f62d95efdb29a09ebe2bace018d +commit 13e157e1f666206fca297c48bec82f3f16ae3c2f Author: local Date: Thu Apr 10 08:54:56 2014 +0000 @@ -70,7 +72,7 @@ Date: Thu Apr 10 08:54:56 2014 +0000 unstaged_deleted_file | 1 \- 4 files changed, 2 insertions(+), 1 deletion(\-) -commit 4d4db1b3bfbb42b0c9c87326bd522cc9d6946911 +commit 30791edff23b9a8fecc5f863da134e820a4c9f1a Author: local Date: Thu Apr 10 08:54:56 2014 +0000 @@ -93,6 +95,10 @@ D deleted_file .RE .\} .sp +.SH "CONFIGURATION VARIABLES" +.SS "depot\-tools\&.freeze\-size\-limit" +.sp +This sets the size limit as an integer number of megabytes of untracked files that git\-freeze will be willing to put in suspended animation\&. A 0 or negative limit disables the size\-limit check entirely\&. \fB100\fR by default\&. .SH "SEE ALSO" .sp \fBgit-thaw\fR(1) diff --git a/man/src/git-freeze.txt b/man/src/git-freeze.txt index 4a5383252..f9042519d 100644 --- a/man/src/git-freeze.txt +++ b/man/src/git-freeze.txt @@ -26,10 +26,25 @@ with 'git add', 'git mv', 'git rm', etc.). A commit with the message `FREEZE.unindexed` will contain all changes which were not in your index at the time you ran git freeze (freshly modified files, new files, etc.). +By default `git freeze` will only freeze up to 100MB of untracked files. See +'CONFIGURATION VARIABLES' for more details. + + EXAMPLE ------- demo:1[] + +CONFIGURATION VARIABLES +----------------------- + +depot-tools.freeze-size-limit +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +This sets the size limit as an integer number of megabytes of untracked files +that git-freeze will be willing to put in suspended animation. A 0 or negative +limit disables the size-limit check entirely. *100* by default. + + SEE ALSO -------- linkgit:git-thaw[1] diff --git a/tests/git_common_test.py b/tests/git_common_test.py index 66211796f..c44524dd7 100755 --- a/tests/git_common_test.py +++ b/tests/git_common_test.py @@ -731,6 +731,23 @@ class GitMutableStructuredTest(git_test_utils.GitRepoReadWriteTestBase, CAT DOG """) + def testStatus(self): + def inner(): + dictified_status = lambda: { + k: dict(v._asdict()) # pylint: disable=W0212 + for k, v in self.repo.run(self.gc.status) + } + self.repo.git('mv', 'file', 'cat') + with open('COOL', 'w') as f: + f.write('Super cool file!') + self.assertDictEqual( + dictified_status(), + {'cat': {'lstat': 'R', 'rstat': ' ', 'src': 'file'}, + 'COOL': {'lstat': '?', 'rstat': '?', 'src': 'COOL'}} + ) + + self.repo.run(inner) + class GitFreezeThaw(git_test_utils.GitRepoReadWriteTestBase): @classmethod @@ -809,6 +826,52 @@ class GitFreezeThaw(git_test_utils.GitRepoReadWriteTestBase): self.repo.run(inner) + def testTooBig(self): + def inner(): + self.repo.git('config', 'depot-tools.freeze-size-limit', '1') + with open('bigfile', 'w') as f: + chunk = 'NERDFACE' * 1024 + for _ in xrange(128 * 2 + 1): # Just over 2 mb + f.write(chunk) + _, err = self.repo.capture_stdio(self.gc.freeze) + self.assertIn('too much untracked+unignored', err) + + self.repo.run(inner) + + def testTooBigMultipleFiles(self): + def inner(): + self.repo.git('config', 'depot-tools.freeze-size-limit', '1') + for i in xrange(3): + with open('file%d' % i, 'w') as f: + chunk = 'NERDFACE' * 1024 + for _ in xrange(50): # About 400k + f.write(chunk) + _, err = self.repo.capture_stdio(self.gc.freeze) + self.assertIn('too much untracked+unignored', err) + + self.repo.run(inner) + + def testMerge(self): + def inner(): + self.repo.git('checkout', '-b', 'bad_merge_branch') + with open('bad_merge', 'w') as f: + f.write('bad_merge_left') + self.repo.git('add', 'bad_merge') + self.repo.git('commit', '-m', 'bad_merge') + + self.repo.git('checkout', 'branch_D') + with open('bad_merge', 'w') as f: + f.write('bad_merge_right') + self.repo.git('add', 'bad_merge') + self.repo.git('commit', '-m', 'bad_merge_d') + + self.repo.git('merge', 'bad_merge_branch') + + _, err = self.repo.capture_stdio(self.gc.freeze) + self.assertIn('Cannot freeze unmerged changes', err) + + self.repo.run(inner) + class GitMakeWorkdir(git_test_utils.GitRepoReadOnlyTestBase, GitCommonTestBase): def setUp(self):