Class: Refinement::Changeset

Inherits:
Object
  • Object
show all
Defined in:
lib/refinement/changeset.rb,
lib/refinement/changeset/file_modification.rb

Overview

Represents a set of changes in a repository between a prior revision and the current state

Defined Under Namespace

Classes: FileModification, GitError

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(repository:, modifications:) ⇒ Changeset

Returns a new instance of Changeset.



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/refinement/changeset.rb', line 22

def initialize(repository:, modifications:)
  @repository = repository
  @modifications = self.class.add_directories(modifications).uniq.freeze

  @modified_paths = {}
  @modifications
    .each { |mod| @modified_paths[mod.path] = mod }
    .each { |mod| @modified_paths[mod.prior_path] ||= mod if mod.prior_path }
  @modified_paths.freeze

  @modified_absolute_paths = {}
  @modified_paths
    .each { |path, mod| @modified_absolute_paths[path.expand_path(repository).freeze] = mod }
  @modified_absolute_paths.freeze
end

Instance Attribute Details

#repositoryPathname (readonly)

Returns the path to the repository.

Returns:

  • (Pathname)

    the path to the repository



12
13
14
# File 'lib/refinement/changeset.rb', line 12

def repository
  @repository
end

Class Method Details

.from_git(repository:, base_revision:) ⇒ Changeset

Returns the changes in the given git repository between the given revision and HEAD.

Parameters:

  • repository (Pathname)
  • base_revision (String)

Returns:

  • (Changeset)

    the changes in the given git repository between the given revision and HEAD

Raises:

  • (ArgumentError)


135
136
137
138
139
140
141
142
143
144
# File 'lib/refinement/changeset.rb', line 135

def self.from_git(repository:, base_revision:)
  raise ArgumentError, "must be given a Pathname for repository, got #{repository.inspect}" unless repository.is_a?(Pathname)
  raise ArgumentError, "must be given a String for base_revision, got #{base_revision.inspect}" unless base_revision.is_a?(String)

  merge_base = git!('merge-base', base_revision, 'HEAD', chdir: repository).strip
  diff = git!('diff', '--raw', '-z', merge_base, chdir: repository)
  modifications = parse_raw_diff(diff, repository: repository, base_revision: merge_base).freeze

  new(repository: repository, modifications: modifications)
end

.parse_raw_diff(diff, repository:, base_revision:) ⇒ Array<FileModification>

Parses the raw diff into FileModification objects

Parameters:

  • diff (String)

    a diff generated by ‘git diff –raw -z`

  • repository (Pathname)

    the path to the repository

  • base_revision (String)

    the base revision the diff was constructed agains

Returns:



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/refinement/changeset.rb', line 166

def self.parse_raw_diff(diff, repository:, base_revision:)
  # since we're null separating the chunks (to avoid dealing with path escaping) we have to reconstruct
  # the chunks into individual diff entries. entries always start with a colon so we can use that to signal if
  # we're on a new entry
  parsed_lines = diff.split("\0").each_with_object([]) do |chunk, lines|
    lines << [] if chunk.start_with?(':')
    lines.last << chunk
  end

  parsed_lines.map do |split_line|
    # change chunk (letter + optional similarity percentage) will always be the last part of first line chunk
    change_chunk = split_line[0].split(/\s/).last

    new_path = Pathname(split_line[2]).freeze if split_line[2]
    old_path = Pathname(split_line[1]).freeze
    prior_path = old_path if new_path
    # new path if one exists, else existing path. new path only exists for rename and copy
    changed_path = new_path || old_path

    change_character = change_chunk[0]
    # returns 0 when a similarity percentage isn't specified by git.
    _similarity = change_chunk[1..3].to_i

    FileModification.new(
      path: changed_path,
      type: CHANGE_CHARACTERS[change_character],
      prior_path: prior_path,
      contents_reader: -> { repository.join(changed_path).read },
      prior_contents_reader: lambda {
        git!('show', "#{base_revision}:#{prior_path || changed_path}", chdir: repository)
      }
    )
  end
end

Instance Method Details

#find_modification_for_glob(absolute_glob:) ⇒ FileModification, Nil

Note:

Will only return a single (arbitrary) matching modification, even if there are multiple modifications that match the glob

Returns the modification for the given absolute glob, or ‘nil` if no files matching the glob were modified.

Parameters:

  • absolute_glob (String)

    a glob pattern for absolute paths, suitable for an invocation of ‘Dir.glob`

Returns:

  • (FileModification, Nil)

    the modification for the given absolute glob, or ‘nil` if no files matching the glob were modified



111
112
113
114
115
116
117
118
119
# File 'lib/refinement/changeset.rb', line 111

def find_modification_for_glob(absolute_glob:)
  absolute_globs = dir_glob_equivalent_patterns(absolute_glob)
  _path, modification = modified_absolute_paths.find do |absolute_path, _modification|
    absolute_globs.any? do |glob|
      File.fnmatch?(glob, absolute_path, File::FNM_CASEFOLD | File::FNM_PATHNAME)
    end
  end
  modification
end

#find_modification_for_path(absolute_path:) ⇒ FileModification, Nil

Returns the changeset for the given absolute path, or ‘nil` if the given path is un-modified.

Parameters:

  • absolute_path (Pathname)

Returns:

  • (FileModification, Nil)

    the changeset for the given absolute path, or ‘nil` if the given path is un-modified



59
60
61
# File 'lib/refinement/changeset.rb', line 59

def find_modification_for_path(absolute_path:)
  modified_absolute_paths[absolute_path]
end

#find_modification_for_yaml_keypath(absolute_path:, keypath:) ⇒ FileModification, Nil

Returns a modification and yaml diff for the keypath at the given absolute path, or ‘nil` if the value at the given keypath is un-modified.

Parameters:

  • absolute_path (Pathname)
  • keypath (Array)

Returns:

  • (FileModification, Nil)

    a modification and yaml diff for the keypath at the given absolute path, or ‘nil` if the value at the given keypath is un-modified



125
126
127
128
129
130
# File 'lib/refinement/changeset.rb', line 125

def find_modification_for_yaml_keypath(absolute_path:, keypath:)
  return unless (file_modification = find_modification_for_path(absolute_path: absolute_path))
  diff = file_modification.yaml_diff(keypath)
  return unless diff
  [file_modification, diff]
end