Class: Rails::Credentials::Conflict::MergeStrategy

Inherits:
Object
  • Object
show all
Defined in:
lib/rails/credentials/conflict/merge_strategy.rb,
sig/rails/credentials/conflict/merge_strategy.rbs

Overview

Performs three-way merge of decrypted credentials using git merge-file.

Handles creating conflict markers for manual resolution, detecting unresolved markers, validating YAML, and opening an editor for interactive conflict resolution.

Constant Summary collapse

CONFLICT_MARKER_START =
"<<<<<<<"
CONFLICT_MARKER_END =
">>>>>>>"

Instance Method Summary collapse

Constructor Details

#initialize(encryption_service) ⇒ MergeStrategy



19
20
21
# File 'lib/rails/credentials/conflict/merge_strategy.rb', line 19

def initialize(encryption_service)
  @encryption_service = encryption_service
end

Instance Method Details

#create_conflict_markers(ours_content, base_content, theirs_content, labels:) ⇒ Object

Runs a three-way merge via git merge-file and returns the result.

Returns a Hash with :content (merged text) and :has_conflicts (true when conflict markers are present).

Raises Error when git merge-file exits with a status greater than 1, which indicates an actual error rather than conflicts.



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/rails/credentials/conflict/merge_strategy.rb', line 30

def create_conflict_markers(ours_content, base_content, theirs_content, labels:)
  with_temp_files(ours_content, base_content, theirs_content) do |ours_path, base_path, theirs_path|
    merged_content, status = Open3.capture2(
      "git", "merge-file", "-p", "--diff3",
      "-L", labels[:ours],
      "-L", labels[:base],
      "-L", labels[:theirs],
      ours_path, base_path, theirs_path,
      err: File::NULL
    )

    exit_code = status.exitstatus

    raise Error, "git merge-file failed (exit #{exit_code || "unknown"})" if exit_code.nil? || exit_code > 1

    { content: merged_content, has_conflicts: exit_code == 1 }
  end
end

#has_conflict_markers?(content) ⇒ Boolean

Returns true if the content contains unresolved git conflict markers.



50
51
52
# File 'lib/rails/credentials/conflict/merge_strategy.rb', line 50

def has_conflict_markers?(content)
  content.include?(CONFLICT_MARKER_START) || content.include?(CONFLICT_MARKER_END)
end

#open_editor_for_resolution(merged_content) ⇒ String

Writes merged_content to a temp file, opens it in $EDITOR, and returns the edited content after the editor exits.



63
64
65
66
67
68
69
70
71
72
73
# File 'lib/rails/credentials/conflict/merge_strategy.rb', line 63

def open_editor_for_resolution(merged_content)
  Tempfile.create(["credentials_conflict", ".yml"]) do |tempfile|
    tempfile.write(merged_content)
    tempfile.flush

    editor = ENV.fetch("EDITOR", "vim")
    system(editor, tempfile.path)

    File.read(tempfile.path)
  end
end

#validate_yaml!(content) ⇒ void

This method returns an undefined value.

Parses content as YAML and raises Error if it is invalid.



55
56
57
58
59
# File 'lib/rails/credentials/conflict/merge_strategy.rb', line 55

def validate_yaml!(content)
  YAML.safe_load(content)
rescue Psych::SyntaxError => e
  raise Error, "Resolved content is not valid YAML: #{e.message}"
end

#with_temp_files(*contents) {|paths| ... } ⇒ Object

Yields:

Yield Parameters:

  • paths (String)

Yield Returns:

  • (Object)


77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/rails/credentials/conflict/merge_strategy.rb', line 77

def with_temp_files(*contents)
  temp_files = contents.map.with_index do |content, index|
    file = Tempfile.new(["temp_#{index}", ".yml"])
    file.write(content)
    file.flush
    file
  end

  yield(*temp_files.map(&:path))
ensure
  temp_files&.each do |file|
    file.close
    file.unlink
  end
end