Module: Overcommit::GitRepo

Defined in:
lib/overcommit/git_repo.rb

Overview

Provide a set of utilities for certain interactions with ‘git`.

Defined Under Namespace

Classes: Submodule, SubmoduleStatus

Constant Summary collapse

DIFF_HUNK_REGEX =

Regular expression used to extract diff ranges from hunks of diff output.

/
  ^@@\s
  [^\s]+\s           # Ignore old file range
  \+(\d+)(?:,(\d+))? # Extract range of hunk containing start line and number of lines
  \s@@.*$
/x.freeze
SUBMODULE_STATUS_REGEX =

Regular expression used to extract information from lines of ‘git submodule status` output

/
  ^\s*(?<prefix>[-+U]?)(?<sha1>\w+)
  \s(?<path>[^\s]+?)
  (?:\s\((?<describe>.+)\))?$
/x.freeze

Class Method Summary collapse

Class Method Details

.all_filesArray<String>

Returns the names of all files that are tracked by git.

Returns:

  • (Array<String>)

    list of absolute file paths



137
138
139
140
141
142
# File 'lib/overcommit/git_repo.rb', line 137

def all_files
  `git ls-files`.
    split(/\n/).
    map { |relative_file| File.expand_path(relative_file) }.
    reject { |file| File.directory?(file) } # Exclude submodule directories
end

.branches_containing_commit(commit_ref) ⇒ Array<String>

Returns the names of all branches containing the given commit.

Parameters:

  • commit_ref (String)

    git tree ref that resolves to a commit

Returns:

  • (Array<String>)

    list of branches containing the given commit



274
275
276
277
278
279
# File 'lib/overcommit/git_repo.rb', line 274

def branches_containing_commit(commit_ref)
  `git branch --column=dense --contains #{commit_ref}`.
    sub(/\((HEAD )?detached (from|at) .*?\)/, ''). # ignore detached HEAD
    split(/\s+/).
    reject { |s| s.empty? || s == '*' }
end

.current_branchString

Returns the name of the currently checked out branch.

Returns:

  • (String)


283
284
285
# File 'lib/overcommit/git_repo.rb', line 283

def current_branch
  `git symbolic-ref --short -q HEAD`.chomp
end

.extract_modified_lines(file_path, options) ⇒ Set

Extract the set of modified lines from a given file.

Parameters:

  • file_path (String)
  • options (Hash)

Returns:

  • (Set)

    line numbers that have been modified in file



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/overcommit/git_repo.rb', line 68

def extract_modified_lines(file_path, options)
  lines = Set.new

  flags = '--cached' if options[:staged]
  refs = options[:refs]
  subcmd = options[:subcmd] || 'diff'

  `git #{subcmd} --no-color --no-ext-diff -U0 #{flags} #{refs} -- "#{file_path}"`.
    scan(DIFF_HUNK_REGEX) do |start_line, lines_added|
    lines_added = (lines_added || 1).to_i # When blank, one line was added
    cur_line = start_line.to_i

    lines_added.times do
      lines.add cur_line
      cur_line += 1
    end
  end

  lines
end

.initial_commit?true, false

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

Returns:

  • (true, false)


146
147
148
# File 'lib/overcommit/git_repo.rb', line 146

def initial_commit?
  !Overcommit::Utils.execute(%w[git rev-parse HEAD]).success?
end

.list_files(paths = [], options = {}) ⇒ Array<String>

Returns the names of files in the given paths that are tracked by git.

Parameters:

  • paths (Array<String>) (defaults to: [])

    list of paths to check

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • ref (String) — default: 'HEAD'

    Git ref to check

Returns:

  • (Array<String>)

    list of absolute file paths



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/overcommit/git_repo.rb', line 110

def list_files(paths = [], options = {})
  ref = options[:ref] || 'HEAD'

  result = Overcommit::Utils.execute(%W[git ls-tree --name-only #{ref}], args: paths)
  unless result.success?
    raise Overcommit::Exceptions::Error,
          "Error listing files. EXIT STATUS(es): #{result.statuses}.\n" \
          "STDOUT(s): #{result.stdouts}.\n" \
          "STDERR(s): #{result.stderrs}."
  end

  result.stdout.split(/\n/).
    map { |relative_file| File.expand_path(relative_file) }.
    reject { |file| File.directory?(file) } # Exclude submodule directories
end

.modified_files(options) ⇒ Array<String>

Returns the names of all files that have been modified compared to HEAD.

Parameters:

  • options (Hash)

Returns:

  • (Array<String>)

    list of absolute file paths



93
94
95
96
97
98
99
100
101
102
103
# File 'lib/overcommit/git_repo.rb', line 93

def modified_files(options)
  flags = '--cached' if options[:staged]
  refs = options[:refs]
  subcmd = options[:subcmd] || 'diff'

  `git #{subcmd} --name-only -z --diff-filter=ACMR --ignore-submodules=all #{flags} #{refs}`.
    split("\0").
    map(&:strip).
    reject(&:empty?).
    map { |relative_file| File.expand_path(relative_file) }
end

.restore_cherry_pick_stateObject

Restore any relevant files that were present when repo was in the middle of a cherry-pick.



203
204
205
206
207
208
209
210
211
# File 'lib/overcommit/git_repo.rb', line 203

def restore_cherry_pick_state
  if @cherry_head
    File.open(File.expand_path('CHERRY_PICK_HEAD',
                               Overcommit::Utils.git_dir), 'w') do |f|
      f.write(@cherry_head)
    end
    @cherry_head = nil
  end
end

.restore_merge_stateObject

Restore any relevant files that were present when repo was in the middle of a merge.



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/overcommit/git_repo.rb', line 183

def restore_merge_state
  if @merge_head
    FileUtils.touch(File.expand_path('MERGE_MODE', Overcommit::Utils.git_dir))

    File.open(File.expand_path('MERGE_HEAD', Overcommit::Utils.git_dir), 'w') do |f|
      f.write(@merge_head)
    end
    @merge_head = nil
  end

  if @merge_msg
    File.open(File.expand_path('MERGE_MSG', Overcommit::Utils.git_dir), 'w') do |f|
      f.write("#{@merge_msg}\n")
    end
    @merge_msg = nil
  end
end

.staged_submodule_removalsObject

Returns the submodules that have been staged for removal.

‘git` has an unexpected behavior where removing a submodule without committing (i.e. such that the submodule directory is removed and the changes to the index are staged) and then doing a hard reset results in the index being wiped but the empty directory of the once existent submodule being restored (but with no content).

This prevents restoration of the stash of the submodule index changes, which breaks pre-commit hook restorations of the working index.

Thus we expose this helper so the restoration code can manually delete the directory.



231
232
233
234
235
236
237
238
239
# File 'lib/overcommit/git_repo.rb', line 231

def staged_submodule_removals
  # There were no submodules before, so none could have been removed
  return [] if `git ls-files .gitmodules`.empty?

  previous = submodules(ref: 'HEAD')
  current = submodules

  previous - current
end

.store_cherry_pick_stateObject

Store any relevant files that are present when repo is in the middle of a cherry-pick.

Restored via [#restore_cherry_pick_state].



171
172
173
174
175
176
177
178
179
# File 'lib/overcommit/git_repo.rb', line 171

def store_cherry_pick_state
  cherry_head = `git rev-parse CHERRY_PICK_HEAD 2> #{File::NULL}`.chomp

  # Store the merge state if we're in the middle of resolving a merge
  # conflict. This is necessary since stashing removes the merge state.
  if cherry_head != 'CHERRY_PICK_HEAD'
    @cherry_head = cherry_head
  end
end

.store_merge_stateObject

Store any relevant files that are present when repo is in the middle of a merge.

Restored via [#restore_merge_state].



154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/overcommit/git_repo.rb', line 154

def store_merge_state
  merge_head = `git rev-parse MERGE_HEAD 2> #{File::NULL}`.chomp

  # Store the merge state if we're in the middle of resolving a merge
  # conflict. This is necessary since stashing removes the merge state.
  if merge_head != 'MERGE_HEAD'
    @merge_head = merge_head
  end

  merge_msg_file = File.expand_path('MERGE_MSG', Overcommit::Utils.git_dir)
  @merge_msg = File.open(merge_msg_file).read if File.exist?(merge_msg_file)
end

.submodule_statuses(options = {}) ⇒ Array<SubmoduleStatus>

Returns a list of SubmoduleStatus objects, one for each submodule in the parent repository.

Parameters:

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • recursive (Boolean)

    check submodules recursively

Returns:



53
54
55
56
57
58
59
60
61
# File 'lib/overcommit/git_repo.rb', line 53

def submodule_statuses(options = {})
  flags = '--recursive' if options[:recursive]

  `git submodule status #{flags}`.
    scan(SUBMODULE_STATUS_REGEX).
    map do |prefix, sha1, path, describe|
      SubmoduleStatus.new(prefix, sha1, path, describe)
    end
end

.submodules(options = {}) ⇒ Array<Overcommit::GitRepo::Submodule>

Returns the current set of registered submodules.

Parameters:

  • options (Hash) (defaults to: {})

Returns:



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/overcommit/git_repo.rb', line 245

def submodules(options = {})
  ref = options[:ref]

  modules = []
  IniParse.parse(`git show #{ref}:.gitmodules 2> #{File::NULL}`).each do |section|
    # git < 1.8.5 does not update the .gitmodules file with submodule
    # changes, so when we are looking at the current state of the work tree,
    # we need to check if the submodule actually exists via another method,
    # since the .gitmodules file we parsed does not represent reality.
    if ref.nil? && GIT_VERSION < '1.8.5'
      result = Overcommit::Utils.execute(%W[
        git submodule status #{section['path']}
      ])
      next unless result.success?
    end

    modules << Submodule.new(section['path'], section['url'])
  end

  modules
rescue IniParse::IniParseError => e
  raise Overcommit::Exceptions::GitSubmoduleError,
        "Unable to read submodule information from #{ref}:.gitmodules file: #{e.message}"
end

.tracked?(path) ⇒ true, false

Returns whether the specified file/path is tracked by this repository.

Parameters:

  • path (String)

Returns:

  • (true, false)


130
131
132
# File 'lib/overcommit/git_repo.rb', line 130

def tracked?(path)
  Overcommit::Utils.execute(%W[git ls-files #{path} --error-unmatch]).success?
end