Skip to content

Commit 1c8b179

Browse files
authored
modified_files and modified_lines_in_file on CommitMsg (sds#727)
* Install debugger * Extract stashing before running hooks to a module This way, if another hook context needs this behavior it can be included using Overcommit::HookContext::Helpers::StashUnstashedChanges. * Extract getting file modifications into a module Methods for determining which files were modified and on which lines can be imported into hook context with Overcommit::HookContext::Helpers::FileModifications. * Refactor hook contexts to use new modules * Apply Rubocop fixes * Remove pry and pry-byebug from Gemfile
1 parent 603aa1b commit 1c8b179

File tree

5 files changed

+896
-199
lines changed

5 files changed

+896
-199
lines changed

lib/overcommit/hook_context/commit_msg.rb

+7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
# frozen_string_literal: true
22

3+
require_relative 'pre_commit'
4+
require_relative 'helpers/stash_unstaged_changes'
5+
require_relative 'helpers/file_modifications'
6+
37
module Overcommit::HookContext
48
# Contains helpers related to contextual information used by commit-msg hooks.
59
class CommitMsg < Base
10+
include Overcommit::HookContext::Helpers::StashUnstagedChanges
11+
include Overcommit::HookContext::Helpers::FileModifications
12+
613
def empty_message?
714
commit_message.strip.empty?
815
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# frozen_string_literal: true
2+
3+
module Overcommit::HookContext
4+
module Helpers
5+
# This module contains methods for determining what files were changed and on what unique line
6+
# numbers did the change occur.
7+
module FileModifications
8+
# Returns whether this hook run was triggered by `git commit --amend`
9+
def amendment?
10+
return @amendment unless @amendment.nil?
11+
12+
cmd = Overcommit::Utils.parent_command
13+
return unless cmd
14+
amend_pattern = 'commit(\s.*)?\s--amend(\s|$)'
15+
16+
# Since the ps command can return invalid byte sequences for commands
17+
# containing unicode characters, we replace the offending characters,
18+
# since the pattern we're looking for will consist of ASCII characters
19+
unless cmd.valid_encoding?
20+
cmd = Overcommit::Utils.
21+
parent_command.
22+
encode('UTF-16be', invalid: :replace, replace: '?').
23+
encode('UTF-8')
24+
end
25+
26+
return @amendment if
27+
# True if the command is a commit with the --amend flag
28+
@amendment = !(/\s#{amend_pattern}/ =~ cmd).nil?
29+
30+
# Check for git aliases that call `commit --amend`
31+
`git config --get-regexp "^alias\\." "#{amend_pattern}"`.
32+
scan(/alias\.([-\w]+)/). # Extract the alias
33+
each do |match|
34+
return @amendment if
35+
# True if the command uses a git alias for `commit --amend`
36+
@amendment = !(/git(\.exe)?\s+#{match[0]}/ =~ cmd).nil?
37+
end
38+
39+
@amendment
40+
end
41+
42+
# Get a list of added, copied, or modified files that have been staged.
43+
# Renames and deletions are ignored, since there should be nothing to check.
44+
def modified_files
45+
unless @modified_files
46+
currently_staged = Overcommit::GitRepo.modified_files(staged: true)
47+
@modified_files = currently_staged
48+
49+
# Include files modified in last commit if amending
50+
if amendment?
51+
subcmd = 'show --format=%n'
52+
previously_modified = Overcommit::GitRepo.modified_files(subcmd: subcmd)
53+
@modified_files |= filter_modified_files(previously_modified)
54+
end
55+
end
56+
@modified_files
57+
end
58+
59+
# Returns the set of line numbers corresponding to the lines that were
60+
# changed in a specified file.
61+
def modified_lines_in_file(file)
62+
@modified_lines ||= {}
63+
unless @modified_lines[file]
64+
@modified_lines[file] =
65+
Overcommit::GitRepo.extract_modified_lines(file, staged: true)
66+
67+
# Include lines modified in last commit if amending
68+
if amendment?
69+
subcmd = 'show --format=%n'
70+
@modified_lines[file] +=
71+
Overcommit::GitRepo.extract_modified_lines(file, subcmd: subcmd)
72+
end
73+
end
74+
@modified_lines[file]
75+
end
76+
end
77+
end
78+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# frozen_string_literal: true
2+
3+
module Overcommit::HookContext
4+
module Helpers
5+
# This module contains behavior for stashing unstaged changes before hooks are ran and restoring
6+
# them afterwards
7+
module StashUnstagedChanges
8+
# Stash unstaged contents of files so hooks don't see changes that aren't
9+
# about to be committed.
10+
def setup_environment
11+
store_modified_times
12+
Overcommit::GitRepo.store_merge_state
13+
Overcommit::GitRepo.store_cherry_pick_state
14+
15+
# Don't attempt to stash changes if all changes are staged, as this
16+
# prevents us from modifying files at all, which plays better with
17+
# editors/tools which watch for file changes.
18+
if !initial_commit? && unstaged_changes?
19+
stash_changes
20+
21+
# While running hooks make it appear as if nothing changed
22+
restore_modified_times
23+
end
24+
end
25+
26+
# Restore unstaged changes and reset file modification times so it appears
27+
# as if nothing ever changed.
28+
#
29+
# We want to restore the modification times for each of the files after
30+
# every step to ensure as little time as possible has passed while the
31+
# modification time on the file was newer. This helps us play more nicely
32+
# with file watchers.
33+
def cleanup_environment
34+
if @changes_stashed
35+
clear_working_tree
36+
restore_working_tree
37+
restore_modified_times
38+
end
39+
40+
Overcommit::GitRepo.restore_merge_state
41+
Overcommit::GitRepo.restore_cherry_pick_state
42+
end
43+
44+
private
45+
46+
# Stores the modification times for all modified files to make it appear like
47+
# they never changed.
48+
#
49+
# This prevents (some) editors from complaining about files changing when we
50+
# stash changes before running the hooks.
51+
def store_modified_times
52+
@modified_times = {}
53+
54+
staged_files = modified_files
55+
unstaged_files = Overcommit::GitRepo.modified_files(staged: false)
56+
57+
(staged_files + unstaged_files).each do |file|
58+
next if Overcommit::Utils.broken_symlink?(file)
59+
next unless File.exist?(file) # Ignore renamed files (old file no longer exists)
60+
@modified_times[file] = File.mtime(file)
61+
end
62+
end
63+
64+
# Returns whether the current git branch is empty (has no commits).
65+
def initial_commit?
66+
return @initial_commit unless @initial_commit.nil?
67+
@initial_commit = Overcommit::GitRepo.initial_commit?
68+
end
69+
70+
# Returns whether there are any changes to tracked files which have not yet
71+
# been staged.
72+
def unstaged_changes?
73+
result = Overcommit::Utils.execute(%w[git --no-pager diff --quiet])
74+
!result.success?
75+
end
76+
77+
def stash_changes
78+
@stash_attempted = true
79+
80+
stash_message = "Overcommit: Stash of repo state before hook run at #{Time.now}"
81+
result = Overcommit::Utils.with_environment('GIT_LITERAL_PATHSPECS' => '0') do
82+
Overcommit::Utils.execute(
83+
%w[git -c commit.gpgsign=false stash save --keep-index --quiet] + [stash_message]
84+
)
85+
end
86+
87+
unless result.success?
88+
# Failure to stash in this case is likely due to a configuration
89+
# issue (e.g. author/email not set or GPG signing key incorrect)
90+
raise Overcommit::Exceptions::HookSetupFailed,
91+
"Unable to setup environment for #{hook_script_name} hook run:" \
92+
"\nSTDOUT:#{result.stdout}\nSTDERR:#{result.stderr}"
93+
end
94+
95+
@changes_stashed = `git stash list -1`.include?(stash_message)
96+
end
97+
98+
# Restores the file modification times for all modified files to make it
99+
# appear like they never changed.
100+
def restore_modified_times
101+
@modified_times.each do |file, time|
102+
next if Overcommit::Utils.broken_symlink?(file)
103+
next unless File.exist?(file)
104+
File.utime(time, time, file)
105+
end
106+
end
107+
108+
# Clears the working tree so that the stash can be applied.
109+
def clear_working_tree
110+
removed_submodules = Overcommit::GitRepo.staged_submodule_removals
111+
112+
result = Overcommit::Utils.execute(%w[git reset --hard])
113+
unless result.success?
114+
raise Overcommit::Exceptions::HookCleanupFailed,
115+
"Unable to cleanup working tree after #{hook_script_name} hooks run:" \
116+
"\nSTDOUT:#{result.stdout}\nSTDERR:#{result.stderr}"
117+
end
118+
119+
# Hard-resetting a staged submodule removal results in the index being
120+
# reset but the submodule being restored as an empty directory. This empty
121+
# directory prevents us from stashing on a subsequent run if a hook fails.
122+
#
123+
# Work around this by removing these empty submodule directories as there
124+
# doesn't appear any reason to keep them around.
125+
removed_submodules.each do |submodule|
126+
FileUtils.rmdir(submodule.path)
127+
end
128+
end
129+
130+
# Applies the stash to the working tree to restore the user's state.
131+
def restore_working_tree
132+
result = Overcommit::Utils.execute(%w[git stash pop --index --quiet])
133+
unless result.success?
134+
raise Overcommit::Exceptions::HookCleanupFailed,
135+
"Unable to restore working tree after #{hook_script_name} hooks run:" \
136+
"\nSTDOUT:#{result.stdout}\nSTDERR:#{result.stderr}"
137+
end
138+
end
139+
end
140+
end
141+
end

0 commit comments

Comments
 (0)