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.



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

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



14
15
16
# File 'lib/refinement/changeset.rb', line 14

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)


140
141
142
143
144
145
146
147
148
149
# File 'lib/refinement/changeset.rb', line 140

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:



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
200
201
202
203
204
# File 'lib/refinement/changeset.rb', line 171

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



114
115
116
117
118
119
120
121
122
# File 'lib/refinement/changeset.rb', line 114

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



62
63
64
# File 'lib/refinement/changeset.rb', line 62

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



128
129
130
131
132
133
134
135
# File 'lib/refinement/changeset.rb', line 128

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