class Overcommit::HookContext::PreCommit

Contains helpers related to contextual information used by pre-commit hooks.

This includes staged files, which lines of those files have been modified, etc. It is also responsible for saving/restoring the state of the repo so hooks only inspect staged changes.

Public Instance Methods

amendment?() click to toggle source

Returns whether this hook run was triggered by `git commit –amend`

# File lib/overcommit/hook_context/pre_commit.rb, line 14
def amendment?
  return @amendment unless @amendment.nil?

  cmd = Overcommit::Utils.parent_command
  return unless cmd
  amend_pattern = 'commit(\s.*)?\s--amend(\s|$)'

  # Since the ps command can return invalid byte sequences for commands
  # containing unicode characters, we replace the offending characters,
  # since the pattern we're looking for will consist of ASCII characters
  unless cmd.valid_encoding?
    cmd = Overcommit::Utils.parent_command.encode('UTF-16be', invalid: :replace, replace: '?').
                                           encode('UTF-8')
  end

  return @amendment if
    # True if the command is a commit with the --amend flag
    @amendment = !(/\s#{amend_pattern}/ =~ cmd).nil?

  # Check for git aliases that call `commit --amend`
  `git config --get-regexp "^alias\\." "#{amend_pattern}"`.
    scan(/alias\.([-\w]+)/). # Extract the alias
    each do |match|
      return @amendment if
        # True if the command uses a git alias for `commit --amend`
        @amendment = !(/git(\.exe)?\s+#{match[0]}/ =~ cmd).nil?
    end

  @amendment
end
cleanup_environment() click to toggle source

Restore unstaged changes and reset file modification times so it appears as if nothing ever changed.

We want to restore the modification times for each of the files after every step to ensure as little time as possible has passed while the modification time on the file was newer. This helps us play more nicely with file watchers.

# File lib/overcommit/hook_context/pre_commit.rb, line 70
def cleanup_environment
  if @changes_stashed
    clear_working_tree
    restore_working_tree
    restore_modified_times
  end

  Overcommit::GitRepo.restore_merge_state
  Overcommit::GitRepo.restore_cherry_pick_state
end
initial_commit?() click to toggle source

Returns whether the current git branch is empty (has no commits).

# File lib/overcommit/hook_context/pre_commit.rb, line 117
def initial_commit?
  return @initial_commit unless @initial_commit.nil?
  @initial_commit = Overcommit::GitRepo.initial_commit?
end
modified_files() click to toggle source

Get a list of added, copied, or modified files that have been staged. Renames and deletions are ignored, since there should be nothing to check.

# File lib/overcommit/hook_context/pre_commit.rb, line 83
def modified_files
  unless @modified_files
    currently_staged = Overcommit::GitRepo.modified_files(staged: true)
    @modified_files = currently_staged

    # Include files modified in last commit if amending
    if amendment?
      subcmd = 'show --format=%n'
      previously_modified = Overcommit::GitRepo.modified_files(subcmd: subcmd)
      @modified_files |= filter_modified_files(previously_modified)
    end
  end
  @modified_files
end
modified_lines_in_file(file) click to toggle source

Returns the set of line numbers corresponding to the lines that were changed in a specified file.

# File lib/overcommit/hook_context/pre_commit.rb, line 100
def modified_lines_in_file(file)
  @modified_lines ||= {}
  unless @modified_lines[file]
    @modified_lines[file] =
      Overcommit::GitRepo.extract_modified_lines(file, staged: true)

    # Include lines modified in last commit if amending
    if amendment?
      subcmd = 'show --format=%n'
      @modified_lines[file] +=
        Overcommit::GitRepo.extract_modified_lines(file, subcmd: subcmd)
    end
  end
  @modified_lines[file]
end
setup_environment() click to toggle source

Stash unstaged contents of files so hooks don't see changes that aren't about to be committed.

# File lib/overcommit/hook_context/pre_commit.rb, line 47
def setup_environment
  store_modified_times
  Overcommit::GitRepo.store_merge_state
  Overcommit::GitRepo.store_cherry_pick_state

  # Don't attempt to stash changes if all changes are staged, as this
  # prevents us from modifying files at all, which plays better with
  # editors/tools which watch for file changes.
  if !initial_commit? && unstaged_changes?
    stash_changes

    # While running hooks make it appear as if nothing changed
    restore_modified_times
  end
end

Private Instance Methods

clear_working_tree() click to toggle source

Clears the working tree so that the stash can be applied.

# File lib/overcommit/hook_context/pre_commit.rb, line 146
def clear_working_tree
  removed_submodules = Overcommit::GitRepo.staged_submodule_removals

  result = Overcommit::Utils.execute(%w[git reset --hard])
  unless result.success?
    raise Overcommit::Exceptions::HookCleanupFailed,
          "Unable to cleanup working tree after #{hook_script_name} hooks run:" \
          "\nSTDOUT:#{result.stdout}\nSTDERR:#{result.stderr}"
  end

  # Hard-resetting a staged submodule removal results in the index being
  # reset but the submodule being restored as an empty directory. This empty
  # directory prevents us from stashing on a subsequent run if a hook fails.
  #
  # Work around this by removing these empty submodule directories as there
  # doesn't appear any reason to keep them around.
  removed_submodules.each do |submodule|
    FileUtils.rmdir(submodule.path)
  end
end
restore_modified_times() click to toggle source

Restores the file modification times for all modified files to make it appear like they never changed.

# File lib/overcommit/hook_context/pre_commit.rb, line 204
def restore_modified_times
  @modified_times.each do |file, time|
    next if Overcommit::Utils.broken_symlink?(file)
    next unless File.exist?(file)
    File.utime(time, time, file)
  end
end
restore_working_tree() click to toggle source

Applies the stash to the working tree to restore the user's state.

# File lib/overcommit/hook_context/pre_commit.rb, line 168
def restore_working_tree
  result = Overcommit::Utils.execute(%w[git stash pop --index --quiet])
  unless result.success?
    raise Overcommit::Exceptions::HookCleanupFailed,
          "Unable to restore working tree after #{hook_script_name} hooks run:" \
          "\nSTDOUT:#{result.stdout}\nSTDERR:#{result.stderr}"
  end
end
stash_changes() click to toggle source
# File lib/overcommit/hook_context/pre_commit.rb, line 124
def stash_changes
  @stash_attempted = true

  stash_message = "Overcommit: Stash of repo state before hook run at #{Time.now}"
  result = Overcommit::Utils.with_environment('GIT_LITERAL_PATHSPECS' => '0') do
    Overcommit::Utils.execute(
      %w[git -c commit.gpgsign=false stash save --keep-index --quiet] + [stash_message]
    )
  end

  unless result.success?
    # Failure to stash in this case is likely due to a configuration
    # issue (e.g. author/email not set or GPG signing key incorrect)
    raise Overcommit::Exceptions::HookSetupFailed,
          "Unable to setup environment for #{hook_script_name} hook run:" \
          "\nSTDOUT:#{result.stdout}\nSTDERR:#{result.stderr}"
  end

  @changes_stashed = `git stash list -1`.include?(stash_message)
end
store_modified_times() click to toggle source

Stores the modification times for all modified files to make it appear like they never changed.

This prevents (some) editors from complaining about files changing when we stash changes before running the hooks.

# File lib/overcommit/hook_context/pre_commit.rb, line 189
def store_modified_times
  @modified_times = {}

  staged_files = modified_files
  unstaged_files = Overcommit::GitRepo.modified_files(staged: false)

  (staged_files + unstaged_files).each do |file|
    next if Overcommit::Utils.broken_symlink?(file)
    next unless File.exist?(file) # Ignore renamed files (old file no longer exists)
    @modified_times[file] = File.mtime(file)
  end
end
unstaged_changes?() click to toggle source

Returns whether there are any changes to tracked files which have not yet been staged.

# File lib/overcommit/hook_context/pre_commit.rb, line 179
def unstaged_changes?
  result = Overcommit::Utils.execute(%w[git --no-pager diff --quiet])
  !result.success?
end