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:, description: nil) ⇒ Changeset

Returns a new instance of Changeset.



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

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

  @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

#descriptionString (readonly)

Returns a desciption of the changeset.

Returns:

  • (String)

    a desciption of the changeset



22
23
24
# File 'lib/refinement/changeset.rb', line 22

def description
  @description
end

#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)


143
144
145
146
147
148
149
150
151
152
# File 'lib/refinement/changeset.rb', line 143

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, description: "since #{base_revision}")
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:



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
205
206
207
# File 'lib/refinement/changeset.rb', line 174

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



117
118
119
120
121
122
123
124
125
# File 'lib/refinement/changeset.rb', line 117

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



65
66
67
# File 'lib/refinement/changeset.rb', line 65

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



131
132
133
134
135
136
137
138
# File 'lib/refinement/changeset.rb', line 131

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