Skip to content

Commit 1dfbd2a

Browse files
sdsShane da Silva
authored and
Shane da Silva
committed
Save/restore unstaged changes before running hooks
Add support for "hiding" unstaged changes to files before running the hooks against them. This prevents the situation where unstaged changes would cause hooks to fail, but they shouldn't because they are unchanged. Previously, Overcommit accomplished this by copying staged changes to temporary files and then running checks against those files. The problem with this approach is that it breaks the functionality of tools which perform certain checks based on filename/path. Since the temporary file will not share the original path, the tool doesn't work as expected. By stashing changes to the file instead, the path/filename remains the same, but we still get the benefit of hiding the unstaged changes. Change-Id: Id1d39d47be4be7348c4126a6d6793506605e8149 Reviewed-on: http://gerrit.causes.com/35603 Reviewed-by: Shane da Silva <shane@causes.com> Tested-by: Shane da Silva <shane@causes.com>
1 parent 35df104 commit 1dfbd2a

File tree

5 files changed

+195
-27
lines changed

5 files changed

+195
-27
lines changed

lib/overcommit/hook_context/base.rb

+17
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,23 @@ def hook_type_name
2525
Overcommit::Utils.snake_case(hook_class_name)
2626
end
2727

28+
# Initializes anything related to the environment.
29+
#
30+
# This is called before the hooks are run by the [HookRunner]. Different
31+
# hook types can perform different setup.
32+
def setup_environment
33+
# Implemented by subclass
34+
end
35+
36+
# Resets the environment to an appropriate state.
37+
#
38+
# This is called after the hooks have been run by the [HookRunner].
39+
# Different hook types can perform different cleanup operations, which are
40+
# intended to "undo" the results of the call to {#setup_environment}.
41+
def cleanup_environment
42+
# Implemented by subclass
43+
end
44+
2845
# Returns a list of files that have been modified.
2946
#
3047
# By default, this returns an empty list. Subclasses should implement if

lib/overcommit/hook_context/pre_commit.rb

+44
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,30 @@ module Overcommit::HookContext
44
# This includes staged files, which lines of those files have been modified,
55
# etc.
66
class PreCommit < Base
7+
# Stash unstaged contents of files so hooks don't see changes that aren't
8+
# about to be committed.
9+
def setup_environment
10+
store_modified_times
11+
12+
if modified_files.any?
13+
`git stash save --keep-index --quiet #{<<-MSG}`
14+
"Overcommit: Stash of repo state before hook run at #{Time.now}"
15+
MSG
16+
end
17+
18+
# While running the hooks make it appear as if nothing changed
19+
restore_modified_times
20+
end
21+
22+
# Restore unstaged changes and reset file modification times so it appears
23+
# as if nothing ever changed.
24+
def cleanup_environment
25+
`git reset --hard`
26+
`git stash pop --index --quiet` if modified_files.any?
27+
28+
restore_modified_times
29+
end
30+
731
# Get a list of added, copied, or modified files that have been staged.
832
# Renames and deletions are ignored, since there should be nothing to check.
933
def modified_files
@@ -46,5 +70,25 @@ def extract_modified_lines(staged_file)
4670

4771
lines
4872
end
73+
74+
def store_modified_times
75+
@modified_times = {}
76+
77+
modified_files.each do |file|
78+
@modified_times[file] = File.mtime(file)
79+
end
80+
end
81+
82+
# Stores the modification times for all modified files to make it appear like
83+
# they never changed.
84+
#
85+
# This prevents editors from complaining about files changing when we stash
86+
# changes before running the hooks.
87+
def restore_modified_times
88+
modified_files.each do |file|
89+
time = @modified_times[file]
90+
File.utime(time, time, file)
91+
end
92+
end
4993
end
5094
end

lib/overcommit/hook_runner.rb

+5-25
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,20 @@ module Overcommit
22
# Responsible for loading the hooks the repository has configured and running
33
# them, collecting and displaying the results.
44
class HookRunner
5-
def initialize(config, logger)
5+
def initialize(config, logger, context)
66
@config = config
77
@logger = logger
8+
@context = context
89
@hooks = []
910
end
1011

1112
# Loads and runs the hooks registered for this {HookRunner}.
12-
def run(context)
13-
@context = context
14-
15-
# stash_unstaged_files
13+
def run
1614
load_hooks
15+
@context.setup_environment
1716
run_hooks
1817
ensure
19-
# restore_unstaged_files
18+
@context.cleanup_environment
2019
end
2120

2221
private
@@ -82,24 +81,5 @@ def create_hook(hook_name)
8281
"Unable to load hook '#{hook_name}': #{error}",
8382
error.backtrace
8483
end
85-
86-
# Stashes untracked files and unstaged changes so that those changes aren't
87-
# read by the hooks.
88-
def stash_unstaged_files
89-
# TODO: store mtime of all stashed files with File.new('blah').mtime
90-
`git stash save --keep-index --include-untracked --quiet #{<<-MSG}`
91-
"Stash of repo state before hook run - #{Time.now}"
92-
MSG
93-
end
94-
95-
# Restores the stashed files after hook has run.
96-
#
97-
# Assumes that any hooks that might have manipulated the stash have properly
98-
# left the stash in its original state.
99-
def restore_unstaged_files
100-
# TODO: Restore mtime of all stashed files with
101-
# FileUtils.touch('blah'), :mtime => time
102-
`git stash pop --quiet`
103-
end
10484
end
10585
end

spec/overcommit/hook_context/pre_commit_spec.rb

+127
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,133 @@
77
let(:input) { '' }
88
let(:context) { described_class.new(config, args, input) }
99

10+
describe '#setup_environment' do
11+
subject { context.setup_environment }
12+
13+
context 'when there are no staged changes' do
14+
around do |example|
15+
repo do
16+
`echo "Hello World" > tracked-file`
17+
`git add tracked-file`
18+
`git commit -m "Add tracked-file"`
19+
`echo "Hello Again" > untracked-file`
20+
example.run
21+
end
22+
end
23+
24+
it 'keeps already-committed files' do
25+
subject
26+
File.open('tracked-file', 'r').read == 'Hello World'
27+
end
28+
29+
it 'keeps untracked files' do
30+
subject
31+
File.open('untracked-file', 'r').read == 'Hello Again'
32+
end
33+
34+
it 'keeps modification times the same' do
35+
expect { subject }.
36+
to_not change { [File.mtime('tracked-file'), File.mtime('untracked-file')] }
37+
end
38+
end
39+
40+
context 'when there are staged changes' do
41+
around do |example|
42+
repo do
43+
`echo "Hello World" > tracked-file`
44+
`git add tracked-file`
45+
`git commit -m "Add tracked-file"`
46+
`echo "Hello Again" > untracked-file`
47+
`echo "Some more text" >> tracked-file`
48+
`git add tracked-file`
49+
`echo "Yet some more text" >> tracked-file`
50+
example.run
51+
end
52+
end
53+
54+
it 'keeps staged changes' do
55+
subject
56+
File.open('tracked-file', 'r').read == 'Hello WorldSome more text'
57+
end
58+
59+
it 'keeps untracked files' do
60+
subject
61+
File.open('untracked-file', 'r').read == 'Hello Again'
62+
end
63+
64+
it 'keeps modification times the same' do
65+
expect { subject }.
66+
to_not change { [File.mtime('tracked-file'), File.mtime('untracked-file')] }
67+
end
68+
end
69+
end
70+
71+
describe '#cleanup_environment' do
72+
subject { context.cleanup_environment }
73+
74+
before do
75+
context.setup_environment
76+
end
77+
78+
context 'when there were no staged changes' do
79+
around do |example|
80+
repo do
81+
`echo "Hello World" > tracked-file`
82+
`git add tracked-file`
83+
`git commit -m "Add tracked-file"`
84+
`echo "Hello Again" > untracked-file`
85+
example.run
86+
end
87+
end
88+
89+
it 'keeps already-committed files' do
90+
subject
91+
File.open('tracked-file', 'r').read.should == "Hello World\n"
92+
end
93+
94+
it 'keeps untracked files' do
95+
subject
96+
File.open('untracked-file', 'r').read.should == "Hello Again\n"
97+
end
98+
99+
it 'keeps modification times the same' do
100+
expect { subject }.
101+
to_not change { [File.mtime('tracked-file'), File.mtime('untracked-file')] }
102+
end
103+
end
104+
105+
context 'when there were staged changes' do
106+
around do |example|
107+
repo do
108+
`echo "Hello World" > tracked-file`
109+
`git add tracked-file`
110+
`git commit -m "Add tracked-file"`
111+
`echo "Hello Again" > untracked-file`
112+
`echo "Some more text" >> tracked-file`
113+
`git add tracked-file`
114+
`echo "Yet some more text" >> tracked-file`
115+
example.run
116+
end
117+
end
118+
119+
it 'restores the unstaged changes' do
120+
subject
121+
File.open('tracked-file', 'r').read.
122+
should == "Hello World\nSome more text\nYet some more text\n"
123+
end
124+
125+
it 'keeps untracked files' do
126+
subject
127+
File.open('untracked-file', 'r').read.should == "Hello Again\n"
128+
end
129+
130+
it 'keeps modification times the same' do
131+
expect { subject }.
132+
to_not change { [File.mtime('tracked-file'), File.mtime('untracked-file')] }
133+
end
134+
end
135+
end
136+
10137
describe '#modified_files' do
11138
subject { context.modified_files }
12139

template-dir/hooks/overcommit-hook

+2-2
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ begin
4848

4949
context = Overcommit::HookContext.create(hook_type_class, config, ARGV, STDIN)
5050
logger = Overcommit::Logger.new(STDOUT)
51-
runner = Overcommit::HookRunner.new(config, logger)
51+
runner = Overcommit::HookRunner.new(config, logger, context)
5252

53-
status = runner.run(context)
53+
status = runner.run
5454

5555
exit(status ? 0 : 65) # 65 = EX_DATAERR
5656
rescue Overcommit::Exceptions::ConfigurationError => error

0 commit comments

Comments
 (0)