Class: Sq::Refinement::Changeset

Inherits:
Object
  • Object
show all
Defined in:
lib/sq/refinement/changeset.rb,
lib/sq/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.



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

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



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

def description
  @description
end

#repositoryPathname (readonly)

Returns the path to the repository.

Returns:

  • (Pathname)

    the path to the repository



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

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)


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

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:, base_revision: merge_base).freeze

  new(repository:, 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:



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
208
# File 'lib/sq/refinement/changeset.rb', line 175

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:,
      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



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

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



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

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



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

def find_modification_for_yaml_keypath(absolute_path:, keypath:)
  return unless (file_modification = find_modification_for_path(absolute_path:))

  diff = file_modification.yaml_diff(keypath)
  return unless diff

  [file_modification, diff]
end