rails-credentials-conflict 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +138 -0
- data/Rakefile +12 -0
- data/lib/rails/credentials/conflict/encryption_service.rb +67 -0
- data/lib/rails/credentials/conflict/git_conflict_handler.rb +125 -0
- data/lib/rails/credentials/conflict/merge_strategy.rb +95 -0
- data/lib/rails/credentials/conflict/path_resolver.rb +51 -0
- data/lib/rails/credentials/conflict/railtie.rb +15 -0
- data/lib/rails/credentials/conflict/resolver.rb +122 -0
- data/lib/rails/credentials/conflict/version.rb +9 -0
- data/lib/rails/credentials/conflict.rb +20 -0
- data/lib/tasks/credentials_conflict.rake +35 -0
- data/rbs_collection.lock.yaml +224 -0
- data/rbs_collection.yaml +19 -0
- data/sig/rails/credentials/conflict/encryption_service.rbs +31 -0
- data/sig/rails/credentials/conflict/git_conflict_handler.rbs +44 -0
- data/sig/rails/credentials/conflict/merge_strategy.rbs +35 -0
- data/sig/rails/credentials/conflict/path_resolver.rbs +19 -0
- data/sig/rails/credentials/conflict/railtie.rbs +8 -0
- data/sig/rails/credentials/conflict/resolver.rbs +38 -0
- data/sig/rails/credentials/conflict.rbs +10 -0
- metadata +95 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 69ad41e2d662e231c9cc58895b567280a3f1c78943dd4dfd389f8b83db21ed73
|
|
4
|
+
data.tar.gz: 1ea33b52f592947663ad9c24c0cf8162c284e2caf91fcf2555c8e8d55c8b6874
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 16863610cb7967fb70d71996dbc88f347ef442464be96462e6387df629c9fe5899de3fead02debc6180abc515250c214a968ff21bcfd2c383527a4ec2c37ef9a
|
|
7
|
+
data.tar.gz: '0936c701ef1b31c7d3724f9a3257ec4e41fc6736ccd7547a06d6a05228531ff5190a0123443462026f866ca3aa87172e5df3fb0c2e64dbcd6438d172e2be8e28'
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2025-01-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial release
|
|
13
|
+
- Rake tasks for resolving encrypted credentials conflicts: `resolve`, `yours`, `theirs`, `base`
|
|
14
|
+
- Three-way merge with conflict markers and editor-based resolution
|
|
15
|
+
- Support for environment-specific credentials
|
|
16
|
+
- YAML validation of resolved content
|
|
17
|
+
- Automatic staging of resolved files
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 jwo1f
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Rails Credentials Conflict
|
|
2
|
+
|
|
3
|
+
A Rails gem that helps resolve git merge conflicts in encrypted credentials files by decrypting, merging, and re-encrypting them.
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
When working with Rails encrypted credentials in a team environment, git merge conflicts in `.yml.enc` files are common. Since these files are encrypted, git cannot automatically merge them, and the conflict markers appear in the encrypted content, making manual resolution impossible.
|
|
8
|
+
|
|
9
|
+
## Solution
|
|
10
|
+
|
|
11
|
+
This gem provides rake tasks to:
|
|
12
|
+
- Decrypt both versions of conflicted credentials
|
|
13
|
+
- Present them in a standard git conflict format for easy resolution
|
|
14
|
+
- Re-encrypt the resolved content
|
|
15
|
+
- Or simply choose one version over the other
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Add this line to your application's Gemfile:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
gem 'rails-credentials-conflict'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
And then execute:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bundle install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
When you encounter a merge conflict in your credentials file, you have four options:
|
|
34
|
+
|
|
35
|
+
### 1. Resolve conflicts manually
|
|
36
|
+
|
|
37
|
+
This command decrypts both versions, opens them in your editor with git conflict markers, and re-encrypts after you've resolved the conflicts:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# For main credentials
|
|
41
|
+
rails credentials:conflict:resolve
|
|
42
|
+
|
|
43
|
+
# For environment-specific credentials
|
|
44
|
+
rails credentials:conflict:resolve[staging]
|
|
45
|
+
rails credentials:conflict:resolve[production]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The command will:
|
|
49
|
+
1. Detect the conflict in the encrypted file
|
|
50
|
+
2. Decrypt both versions (yours and theirs)
|
|
51
|
+
3. Compare them - if identical, auto-merge
|
|
52
|
+
4. If different, create a temporary file with git conflict markers:
|
|
53
|
+
```yaml
|
|
54
|
+
<<<<<<< HEAD (yours)
|
|
55
|
+
api_key: your-key
|
|
56
|
+
=======
|
|
57
|
+
api_key: their-key
|
|
58
|
+
>>>>>>> MERGE_HEAD (theirs)
|
|
59
|
+
```
|
|
60
|
+
5. Open the file in your `$EDITOR` (defaults to vim)
|
|
61
|
+
6. After you save and close, validate YAML and re-encrypt the resolved content
|
|
62
|
+
7. Stage the resolved file with git
|
|
63
|
+
|
|
64
|
+
### 2. Keep your version
|
|
65
|
+
|
|
66
|
+
To discard their changes and keep only your version:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# For main credentials
|
|
70
|
+
rails credentials:conflict:yours
|
|
71
|
+
|
|
72
|
+
# For environment-specific credentials
|
|
73
|
+
rails credentials:conflict:yours[staging]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 3. Keep their version
|
|
77
|
+
|
|
78
|
+
To discard your changes and keep only their version:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# For main credentials
|
|
82
|
+
rails credentials:conflict:theirs
|
|
83
|
+
|
|
84
|
+
# For environment-specific credentials
|
|
85
|
+
rails credentials:conflict:theirs[production]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 4. Keep base version
|
|
89
|
+
|
|
90
|
+
To keep the base version (shown in the middle section of 3-way diff):
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# For main credentials
|
|
94
|
+
rails credentials:conflict:base
|
|
95
|
+
|
|
96
|
+
# For environment-specific credentials
|
|
97
|
+
rails credentials:conflict:base[staging]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## How it works
|
|
101
|
+
|
|
102
|
+
The gem uses git's staging area to access all versions of the conflicted file:
|
|
103
|
+
- Stage 1 contains "base" (merge-base/common ancestor)
|
|
104
|
+
- Stage 2 contains "ours" (your version)
|
|
105
|
+
- Stage 3 contains "theirs" (their version)
|
|
106
|
+
|
|
107
|
+
It then:
|
|
108
|
+
1. Decrypts the versions using your local key
|
|
109
|
+
2. Performs the requested operation (merge, yours, theirs, or base)
|
|
110
|
+
3. Validates the resolved YAML
|
|
111
|
+
4. Re-encrypts the result
|
|
112
|
+
5. Stages the resolved file
|
|
113
|
+
|
|
114
|
+
Works with merge, rebase, and cherry-pick conflicts.
|
|
115
|
+
|
|
116
|
+
## Requirements
|
|
117
|
+
|
|
118
|
+
- Ruby >= 3.1.0
|
|
119
|
+
- Rails >= 6.0
|
|
120
|
+
- Git repository with encrypted credentials
|
|
121
|
+
- The appropriate encryption key must be available via one of:
|
|
122
|
+
- `RAILS_MASTER_KEY` environment variable
|
|
123
|
+
- `config/master.key` for main credentials
|
|
124
|
+
- `config/credentials/<environment>.key` for environment-specific credentials
|
|
125
|
+
|
|
126
|
+
## Development
|
|
127
|
+
|
|
128
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
|
129
|
+
|
|
130
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
131
|
+
|
|
132
|
+
## Contributing
|
|
133
|
+
|
|
134
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/jwo1f/rails-credentials-conflict.
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/message_encryptor"
|
|
4
|
+
|
|
5
|
+
module Rails
|
|
6
|
+
module Credentials
|
|
7
|
+
module Conflict
|
|
8
|
+
# Encrypts and decrypts Rails credentials content using AES-128-GCM.
|
|
9
|
+
#
|
|
10
|
+
# The encryption key is read from either an environment variable
|
|
11
|
+
# (default +RAILS_MASTER_KEY+) or a key file on disk.
|
|
12
|
+
class EncryptionService
|
|
13
|
+
CIPHER = "aes-128-gcm"
|
|
14
|
+
|
|
15
|
+
def initialize(key_path, env_key: "RAILS_MASTER_KEY")
|
|
16
|
+
@key_path = key_path
|
|
17
|
+
@env_key = env_key
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Decrypts and verifies +encrypted_content+.
|
|
21
|
+
# Returns an empty string for nil or empty input.
|
|
22
|
+
def decrypt(encrypted_content)
|
|
23
|
+
return "" if encrypted_content.nil? || encrypted_content.empty?
|
|
24
|
+
|
|
25
|
+
encryptor.decrypt_and_verify(encrypted_content)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Encrypts and signs +content+, returning the ciphertext.
|
|
29
|
+
def encrypt(content)
|
|
30
|
+
encryptor.encrypt_and_sign(content)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Encrypts +content+ and writes it to +destination_path+ in binary mode.
|
|
34
|
+
def save_encrypted(content, destination_path)
|
|
35
|
+
File.binwrite(destination_path, encrypt(content))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def encryptor
|
|
41
|
+
@encryptor ||= ActiveSupport::MessageEncryptor.new(
|
|
42
|
+
[read_key].pack("H*"),
|
|
43
|
+
cipher: CIPHER
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def read_key
|
|
48
|
+
key_from_env || key_from_file || raise(
|
|
49
|
+
Error, "Encryption key not found. Set #{@env_key} env variable or create #{@key_path}"
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def key_from_env
|
|
54
|
+
value = ENV[@env_key]&.strip
|
|
55
|
+
value unless value.nil? || value.empty?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def key_from_file
|
|
59
|
+
return nil unless File.exist?(@key_path)
|
|
60
|
+
|
|
61
|
+
value = File.read(@key_path).strip
|
|
62
|
+
value.empty? ? nil : value
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Rails
|
|
6
|
+
module Credentials
|
|
7
|
+
module Conflict
|
|
8
|
+
# Interacts with git to read conflict stages, detect merge state,
|
|
9
|
+
# and stage resolved files.
|
|
10
|
+
#
|
|
11
|
+
# Uses git's staging area to access the three versions of a
|
|
12
|
+
# conflicted file (base, ours, theirs) and determines labels
|
|
13
|
+
# for the merge participants.
|
|
14
|
+
class GitConflictHandler
|
|
15
|
+
STAGE_BASE = 1
|
|
16
|
+
STAGE_OURS = 2
|
|
17
|
+
STAGE_THEIRS = 3
|
|
18
|
+
|
|
19
|
+
def initialize(credentials_path, relative_path)
|
|
20
|
+
@credentials_path = credentials_path.to_s
|
|
21
|
+
@relative_path = relative_path.to_s
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Verifies that the credentials file exists and is in a conflicted
|
|
25
|
+
# state (UU or AA in +git status --porcelain+).
|
|
26
|
+
# Raises +Error+ otherwise.
|
|
27
|
+
def validate_conflict!
|
|
28
|
+
raise Error, "Credentials file not found: #{@credentials_path}" unless File.exist?(@credentials_path)
|
|
29
|
+
|
|
30
|
+
git_status, status = Open3.capture2("git", "status", "--porcelain", @credentials_path)
|
|
31
|
+
git_status = git_status.strip
|
|
32
|
+
|
|
33
|
+
return if status.success? && (git_status.start_with?("UU") || git_status.start_with?("AA"))
|
|
34
|
+
|
|
35
|
+
raise Error, "No git conflict detected for #{@credentials_path}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Retrieves the encrypted content for the given +version+ (:base, :ours, or :theirs)
|
|
39
|
+
# from the git staging area. Returns an empty string on failure.
|
|
40
|
+
def get_version(version)
|
|
41
|
+
stage_number = stage_for_version(version)
|
|
42
|
+
content, status = Open3.capture2(
|
|
43
|
+
"git", "show", ":#{stage_number}:#{@relative_path}",
|
|
44
|
+
binmode: true
|
|
45
|
+
)
|
|
46
|
+
status.success? ? content.strip : ""
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Stages the resolved credentials file with +git add+.
|
|
50
|
+
# Raises +Error+ if the command fails.
|
|
51
|
+
def stage_resolved_file!
|
|
52
|
+
_, status = Open3.capture2("git", "add", @credentials_path)
|
|
53
|
+
|
|
54
|
+
return if status.success?
|
|
55
|
+
|
|
56
|
+
raise Error, "Failed to stage resolved file: #{@credentials_path}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns a Hash of labels for the three merge participants
|
|
60
|
+
# (+:ours+, +:base+, +:theirs+), each containing a branch name
|
|
61
|
+
# and short SHA.
|
|
62
|
+
def get_merge_labels
|
|
63
|
+
head_ref = detect_head_ref
|
|
64
|
+
|
|
65
|
+
{
|
|
66
|
+
ours: build_label(get_current_branch, get_short_sha("HEAD")),
|
|
67
|
+
base: build_label("ancestor", get_merge_base_sha(head_ref)),
|
|
68
|
+
theirs: build_label(get_incoming_branch(head_ref), get_short_sha(head_ref))
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def stage_for_version(version)
|
|
75
|
+
case version
|
|
76
|
+
when :base then STAGE_BASE
|
|
77
|
+
when :ours then STAGE_OURS
|
|
78
|
+
when :theirs then STAGE_THEIRS
|
|
79
|
+
else raise Error, "Unknown version: #{version}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def detect_head_ref
|
|
84
|
+
%w[MERGE_HEAD CHERRY_PICK_HEAD REBASE_HEAD].each do |ref|
|
|
85
|
+
_, status = Open3.capture2("git", "rev-parse", "--verify", ref)
|
|
86
|
+
return ref if status.success?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
"MERGE_HEAD"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def build_label(branch_name, commit_sha)
|
|
93
|
+
"#{branch_name} (#{commit_sha})"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def get_current_branch
|
|
97
|
+
branch, status = Open3.capture2("git", "rev-parse", "--abbrev-ref", "HEAD")
|
|
98
|
+
branch = branch.strip
|
|
99
|
+
status.success? && !branch.empty? ? branch : "HEAD"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def get_incoming_branch(head_ref)
|
|
103
|
+
branch, status = Open3.capture2("git", "name-rev", "--name-only", head_ref)
|
|
104
|
+
branch = branch.strip
|
|
105
|
+
status.success? && !branch.empty? ? branch : head_ref
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def get_short_sha(ref)
|
|
109
|
+
sha, status = Open3.capture2("git", "rev-parse", "--short=8", ref)
|
|
110
|
+
status.success? ? sha.strip : "unknown"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def get_merge_base_sha(head_ref)
|
|
114
|
+
ours_sha, s1 = Open3.capture2("git", "rev-parse", "HEAD")
|
|
115
|
+
theirs_sha, s2 = Open3.capture2("git", "rev-parse", head_ref)
|
|
116
|
+
|
|
117
|
+
return "unknown" unless s1.success? && s2.success?
|
|
118
|
+
|
|
119
|
+
base_sha, s3 = Open3.capture2("git", "merge-base", ours_sha.strip, theirs_sha.strip)
|
|
120
|
+
s3.success? ? base_sha.strip[0..7] : "unknown"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
require "yaml"
|
|
6
|
+
|
|
7
|
+
module Rails
|
|
8
|
+
module Credentials
|
|
9
|
+
module Conflict
|
|
10
|
+
# Performs three-way merge of decrypted credentials using git merge-file.
|
|
11
|
+
#
|
|
12
|
+
# Handles creating conflict markers for manual resolution,
|
|
13
|
+
# detecting unresolved markers, validating YAML, and opening
|
|
14
|
+
# an editor for interactive conflict resolution.
|
|
15
|
+
class MergeStrategy
|
|
16
|
+
CONFLICT_MARKER_START = "<<<<<<<"
|
|
17
|
+
CONFLICT_MARKER_END = ">>>>>>>"
|
|
18
|
+
|
|
19
|
+
def initialize(encryption_service)
|
|
20
|
+
@encryption_service = encryption_service
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Runs a three-way merge via +git merge-file+ and returns the result.
|
|
24
|
+
#
|
|
25
|
+
# Returns a Hash with +:content+ (merged text) and +:has_conflicts+
|
|
26
|
+
# (true when conflict markers are present).
|
|
27
|
+
#
|
|
28
|
+
# Raises +Error+ when +git merge-file+ exits with a status greater
|
|
29
|
+
# than 1, which indicates an actual error rather than conflicts.
|
|
30
|
+
def create_conflict_markers(ours_content, base_content, theirs_content, labels:)
|
|
31
|
+
with_temp_files(ours_content, base_content, theirs_content) do |ours_path, base_path, theirs_path|
|
|
32
|
+
merged_content, status = Open3.capture2(
|
|
33
|
+
"git", "merge-file", "-p", "--diff3",
|
|
34
|
+
"-L", labels[:ours],
|
|
35
|
+
"-L", labels[:base],
|
|
36
|
+
"-L", labels[:theirs],
|
|
37
|
+
ours_path, base_path, theirs_path,
|
|
38
|
+
err: File::NULL
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
exit_code = status.exitstatus
|
|
42
|
+
|
|
43
|
+
raise Error, "git merge-file failed (exit #{exit_code || "unknown"})" if exit_code.nil? || exit_code > 1
|
|
44
|
+
|
|
45
|
+
{ content: merged_content, has_conflicts: exit_code == 1 }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns true if the content contains unresolved git conflict markers.
|
|
50
|
+
def has_conflict_markers?(content)
|
|
51
|
+
content.include?(CONFLICT_MARKER_START) || content.include?(CONFLICT_MARKER_END)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Parses content as YAML and raises +Error+ if it is invalid.
|
|
55
|
+
def validate_yaml!(content)
|
|
56
|
+
YAML.safe_load(content)
|
|
57
|
+
rescue Psych::SyntaxError => e
|
|
58
|
+
raise Error, "Resolved content is not valid YAML: #{e.message}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Writes +merged_content+ to a temp file, opens it in +$EDITOR+,
|
|
62
|
+
# and returns the edited content after the editor exits.
|
|
63
|
+
def open_editor_for_resolution(merged_content)
|
|
64
|
+
Tempfile.create(["credentials_conflict", ".yml"]) do |tempfile|
|
|
65
|
+
tempfile.write(merged_content)
|
|
66
|
+
tempfile.flush
|
|
67
|
+
|
|
68
|
+
editor = ENV.fetch("EDITOR", "vim")
|
|
69
|
+
system(editor, tempfile.path)
|
|
70
|
+
|
|
71
|
+
File.read(tempfile.path)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def with_temp_files(*contents)
|
|
78
|
+
temp_files = contents.map.with_index do |content, index|
|
|
79
|
+
file = Tempfile.new(["temp_#{index}", ".yml"])
|
|
80
|
+
file.write(content)
|
|
81
|
+
file.flush
|
|
82
|
+
file
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
yield(*temp_files.map(&:path))
|
|
86
|
+
ensure
|
|
87
|
+
temp_files&.each do |file|
|
|
88
|
+
file.close
|
|
89
|
+
file.unlink
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rails
|
|
4
|
+
module Credentials
|
|
5
|
+
module Conflict
|
|
6
|
+
# Resolves file system paths for Rails encrypted credentials
|
|
7
|
+
# and their corresponding key files.
|
|
8
|
+
#
|
|
9
|
+
# Supports both the default credentials (+config/credentials.yml.enc+)
|
|
10
|
+
# and environment-specific ones (+config/credentials/<env>.yml.enc+).
|
|
11
|
+
class PathResolver
|
|
12
|
+
VALID_ENVIRONMENT = /\A[a-z0-9_]+\z/
|
|
13
|
+
|
|
14
|
+
attr_reader :environment
|
|
15
|
+
|
|
16
|
+
def initialize(environment = nil)
|
|
17
|
+
if environment && !VALID_ENVIRONMENT.match?(environment.to_s)
|
|
18
|
+
raise Error,
|
|
19
|
+
"Invalid environment name: #{environment.inspect}. " \
|
|
20
|
+
"Must contain only lowercase letters, digits, and underscores."
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
@environment = environment
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns the absolute Pathname to the encrypted credentials file.
|
|
27
|
+
def credentials_path
|
|
28
|
+
if environment
|
|
29
|
+
::Rails.root.join("config", "credentials", "#{environment}.yml.enc")
|
|
30
|
+
else
|
|
31
|
+
::Rails.root.join("config", "credentials.yml.enc")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Returns the absolute Pathname to the encryption key file.
|
|
36
|
+
def key_path
|
|
37
|
+
if environment
|
|
38
|
+
::Rails.root.join("config", "credentials", "#{environment}.key")
|
|
39
|
+
else
|
|
40
|
+
::Rails.root.join("config", "master.key")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns the credentials path relative to +Rails.root+.
|
|
45
|
+
def relative_credentials_path
|
|
46
|
+
credentials_path.relative_path_from(::Rails.root)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rails
|
|
4
|
+
module Credentials
|
|
5
|
+
module Conflict
|
|
6
|
+
# Integrates the gem with Rails by loading the rake tasks
|
|
7
|
+
# when the application boots.
|
|
8
|
+
class Railtie < Rails::Railtie
|
|
9
|
+
rake_tasks do
|
|
10
|
+
load "tasks/credentials_conflict.rake"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rails
|
|
4
|
+
module Credentials
|
|
5
|
+
module Conflict
|
|
6
|
+
# Top-level orchestrator for resolving git merge conflicts in
|
|
7
|
+
# encrypted Rails credentials files.
|
|
8
|
+
#
|
|
9
|
+
# Coordinates between PathResolver, EncryptionService,
|
|
10
|
+
# GitConflictHandler, and MergeStrategy to decrypt conflicting
|
|
11
|
+
# versions, merge them, and re-encrypt the result.
|
|
12
|
+
class Resolver
|
|
13
|
+
attr_reader :environment
|
|
14
|
+
|
|
15
|
+
def initialize(environment = nil, output: $stdout)
|
|
16
|
+
@environment = environment
|
|
17
|
+
@output = output
|
|
18
|
+
@path_resolver = PathResolver.new(environment)
|
|
19
|
+
@encryption_service = EncryptionService.new(@path_resolver.key_path)
|
|
20
|
+
@git_handler = GitConflictHandler.new(
|
|
21
|
+
@path_resolver.credentials_path,
|
|
22
|
+
@path_resolver.relative_credentials_path
|
|
23
|
+
)
|
|
24
|
+
@merge_strategy = MergeStrategy.new(@encryption_service)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Decrypts both sides of a conflict, performs a three-way merge,
|
|
28
|
+
# and re-encrypts the resolved content. Opens an editor when
|
|
29
|
+
# automatic merge is not possible.
|
|
30
|
+
def resolve
|
|
31
|
+
@git_handler.validate_conflict!
|
|
32
|
+
|
|
33
|
+
ours_content = decrypt_version(:ours)
|
|
34
|
+
theirs_content = decrypt_version(:theirs)
|
|
35
|
+
|
|
36
|
+
if ours_content == theirs_content
|
|
37
|
+
log "No conflicts detected. Both versions are identical."
|
|
38
|
+
save_and_stage(ours_content)
|
|
39
|
+
return
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
base_content = get_base_version
|
|
43
|
+
labels = @git_handler.get_merge_labels
|
|
44
|
+
merge_result = @merge_strategy.create_conflict_markers(
|
|
45
|
+
ours_content,
|
|
46
|
+
base_content,
|
|
47
|
+
theirs_content,
|
|
48
|
+
labels: labels
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if merge_result[:has_conflicts]
|
|
52
|
+
log "Conflicts detected. Opening editor for manual resolution..."
|
|
53
|
+
resolved_content = @merge_strategy.open_editor_for_resolution(merge_result[:content])
|
|
54
|
+
|
|
55
|
+
if @merge_strategy.has_conflict_markers?(resolved_content)
|
|
56
|
+
raise Error, "Conflict markers still present. Please resolve all conflicts."
|
|
57
|
+
end
|
|
58
|
+
else
|
|
59
|
+
log "Changes in different sections detected. Auto-merged successfully."
|
|
60
|
+
resolved_content = merge_result[:content]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
@merge_strategy.validate_yaml!(resolved_content)
|
|
64
|
+
save_and_stage(resolved_content)
|
|
65
|
+
log "Credentials successfully resolved and encrypted."
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Resolves the conflict by keeping the current branch's version.
|
|
69
|
+
def yours
|
|
70
|
+
resolve_with_version(:ours, "your")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Resolves the conflict by keeping the incoming branch's version.
|
|
74
|
+
def theirs
|
|
75
|
+
resolve_with_version(:theirs, "their")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Resolves the conflict by keeping the common ancestor's version.
|
|
79
|
+
def base
|
|
80
|
+
resolve_with_version(:base, "base")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def log(message)
|
|
86
|
+
@output.puts(message)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def resolve_with_version(version, label)
|
|
90
|
+
@git_handler.validate_conflict!
|
|
91
|
+
|
|
92
|
+
encrypted_content = @git_handler.get_version(version)
|
|
93
|
+
raise Error, "No #{label} version found." if encrypted_content.empty?
|
|
94
|
+
|
|
95
|
+
content = @encryption_service.decrypt(encrypted_content)
|
|
96
|
+
save_and_stage(content)
|
|
97
|
+
log "Kept #{label} version of credentials."
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def decrypt_version(version)
|
|
101
|
+
encrypted_content = @git_handler.get_version(version)
|
|
102
|
+
@encryption_service.decrypt(encrypted_content)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def get_base_version
|
|
106
|
+
base_encrypted = @git_handler.get_version(:base)
|
|
107
|
+
return "" if base_encrypted.empty?
|
|
108
|
+
|
|
109
|
+
@encryption_service.decrypt(base_encrypted)
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
log "Warning: Could not decrypt base version (#{e.message}). Using empty base."
|
|
112
|
+
""
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def save_and_stage(content)
|
|
116
|
+
@encryption_service.save_encrypted(content, @path_resolver.credentials_path)
|
|
117
|
+
@git_handler.stage_resolved_file!
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "conflict/version"
|
|
4
|
+
require_relative "conflict/path_resolver"
|
|
5
|
+
require_relative "conflict/encryption_service"
|
|
6
|
+
require_relative "conflict/git_conflict_handler"
|
|
7
|
+
require_relative "conflict/merge_strategy"
|
|
8
|
+
require_relative "conflict/resolver"
|
|
9
|
+
require_relative "conflict/railtie" if defined?(Rails::Railtie)
|
|
10
|
+
|
|
11
|
+
module Rails
|
|
12
|
+
module Credentials
|
|
13
|
+
# Provides tooling to resolve git merge conflicts in Rails
|
|
14
|
+
# encrypted credentials files by decrypting, merging, and
|
|
15
|
+
# re-encrypting them.
|
|
16
|
+
module Conflict
|
|
17
|
+
class Error < StandardError; end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/credentials/conflict/resolver"
|
|
4
|
+
|
|
5
|
+
namespace :credentials do
|
|
6
|
+
namespace :conflict do
|
|
7
|
+
desc "Resolve credentials conflict by decrypting and merging"
|
|
8
|
+
task :resolve, [:environment] => :environment do |_t, args|
|
|
9
|
+
Rails::Credentials::Conflict::Resolver.new(args[:environment]).resolve
|
|
10
|
+
rescue Rails::Credentials::Conflict::Error => e
|
|
11
|
+
abort e.message
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
desc "Resolve credentials conflict by keeping yours (usage: rails credentials:conflict:yours[environment])"
|
|
15
|
+
task :yours, [:environment] => :environment do |_t, args|
|
|
16
|
+
Rails::Credentials::Conflict::Resolver.new(args[:environment]).yours
|
|
17
|
+
rescue Rails::Credentials::Conflict::Error => e
|
|
18
|
+
abort e.message
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
desc "Resolve credentials conflict by keeping theirs (usage: rails credentials:conflict:theirs[environment])"
|
|
22
|
+
task :theirs, [:environment] => :environment do |_t, args|
|
|
23
|
+
Rails::Credentials::Conflict::Resolver.new(args[:environment]).theirs
|
|
24
|
+
rescue Rails::Credentials::Conflict::Error => e
|
|
25
|
+
abort e.message
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
desc "Resolve credentials conflict by keeping base version (usage: rails credentials:conflict:base[environment])"
|
|
29
|
+
task :base, [:environment] => :environment do |_t, args|
|
|
30
|
+
Rails::Credentials::Conflict::Resolver.new(args[:environment]).base
|
|
31
|
+
rescue Rails::Credentials::Conflict::Error => e
|
|
32
|
+
abort e.message
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
---
|
|
2
|
+
path: ".gem_rbs_collection"
|
|
3
|
+
gems:
|
|
4
|
+
- name: actionpack
|
|
5
|
+
version: '7.2'
|
|
6
|
+
source:
|
|
7
|
+
type: git
|
|
8
|
+
name: ruby/gem_rbs_collection
|
|
9
|
+
revision: 5aeae33367fa324abf3a4ef912f4e27aaf17f6b9
|
|
10
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
11
|
+
repo_dir: gems
|
|
12
|
+
- name: actionview
|
|
13
|
+
version: '6.0'
|
|
14
|
+
source:
|
|
15
|
+
type: git
|
|
16
|
+
name: ruby/gem_rbs_collection
|
|
17
|
+
revision: 5aeae33367fa324abf3a4ef912f4e27aaf17f6b9
|
|
18
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
19
|
+
repo_dir: gems
|
|
20
|
+
- name: activesupport
|
|
21
|
+
version: '7.0'
|
|
22
|
+
source:
|
|
23
|
+
type: git
|
|
24
|
+
name: ruby/gem_rbs_collection
|
|
25
|
+
revision: 5aeae33367fa324abf3a4ef912f4e27aaf17f6b9
|
|
26
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
27
|
+
repo_dir: gems
|
|
28
|
+
- name: base64
|
|
29
|
+
version: 0.3.0
|
|
30
|
+
source:
|
|
31
|
+
type: rubygems
|
|
32
|
+
- name: bigdecimal
|
|
33
|
+
version: '3.1'
|
|
34
|
+
source:
|
|
35
|
+
type: git
|
|
36
|
+
name: ruby/gem_rbs_collection
|
|
37
|
+
revision: 5aeae33367fa324abf3a4ef912f4e27aaf17f6b9
|
|
38
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
39
|
+
repo_dir: gems
|
|
40
|
+
- name: cgi
|
|
41
|
+
version: '0'
|
|
42
|
+
source:
|
|
43
|
+
type: stdlib
|
|
44
|
+
- name: concurrent-ruby
|
|
45
|
+
version: '1.1'
|
|
46
|
+
source:
|
|
47
|
+
type: git
|
|
48
|
+
name: ruby/gem_rbs_collection
|
|
49
|
+
revision: 5aeae33367fa324abf3a4ef912f4e27aaf17f6b9
|
|
50
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
51
|
+
repo_dir: gems
|
|
52
|
+
- name: connection_pool
|
|
53
|
+
version: '2.4'
|
|
54
|
+
source:
|
|
55
|
+
type: git
|
|
56
|
+
name: ruby/gem_rbs_collection
|
|
57
|
+
revision: 5aeae33367fa324abf3a4ef912f4e27aaf17f6b9
|
|
58
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
59
|
+
repo_dir: gems
|
|
60
|
+
- name: date
|
|
61
|
+
version: '0'
|
|
62
|
+
source:
|
|
63
|
+
type: stdlib
|
|
64
|
+
- name: digest
|
|
65
|
+
version: '0'
|
|
66
|
+
source:
|
|
67
|
+
type: stdlib
|
|
68
|
+
- name: erb
|
|
69
|
+
version: '0'
|
|
70
|
+
source:
|
|
71
|
+
type: stdlib
|
|
72
|
+
- name: fileutils
|
|
73
|
+
version: '0'
|
|
74
|
+
source:
|
|
75
|
+
type: stdlib
|
|
76
|
+
- name: i18n
|
|
77
|
+
version: '1.10'
|
|
78
|
+
source:
|
|
79
|
+
type: git
|
|
80
|
+
name: ruby/gem_rbs_collection
|
|
81
|
+
revision: 5aeae33367fa324abf3a4ef912f4e27aaf17f6b9
|
|
82
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
83
|
+
repo_dir: gems
|
|
84
|
+
- name: io-console
|
|
85
|
+
version: '0'
|
|
86
|
+
source:
|
|
87
|
+
type: stdlib
|
|
88
|
+
- name: json
|
|
89
|
+
version: '0'
|
|
90
|
+
source:
|
|
91
|
+
type: stdlib
|
|
92
|
+
- name: logger
|
|
93
|
+
version: '0'
|
|
94
|
+
source:
|
|
95
|
+
type: stdlib
|
|
96
|
+
- name: minitest
|
|
97
|
+
version: '5.25'
|
|
98
|
+
source:
|
|
99
|
+
type: git
|
|
100
|
+
name: ruby/gem_rbs_collection
|
|
101
|
+
revision: 5aeae33367fa324abf3a4ef912f4e27aaf17f6b9
|
|
102
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
103
|
+
repo_dir: gems
|
|
104
|
+
- name: monitor
|
|
105
|
+
version: '0'
|
|
106
|
+
source:
|
|
107
|
+
type: stdlib
|
|
108
|
+
- name: nokogiri
|
|
109
|
+
version: '1.11'
|
|
110
|
+
source:
|
|
111
|
+
type: git
|
|
112
|
+
name: ruby/gem_rbs_collection
|
|
113
|
+
revision: 5aeae33367fa324abf3a4ef912f4e27aaf17f6b9
|
|
114
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
115
|
+
repo_dir: gems
|
|
116
|
+
- name: openssl
|
|
117
|
+
version: '0'
|
|
118
|
+
source:
|
|
119
|
+
type: stdlib
|
|
120
|
+
- name: pp
|
|
121
|
+
version: '0'
|
|
122
|
+
source:
|
|
123
|
+
type: stdlib
|
|
124
|
+
- name: prettyprint
|
|
125
|
+
version: '0'
|
|
126
|
+
source:
|
|
127
|
+
type: stdlib
|
|
128
|
+
- name: rack
|
|
129
|
+
version: '2.2'
|
|
130
|
+
source:
|
|
131
|
+
type: git
|
|
132
|
+
name: ruby/gem_rbs_collection
|
|
133
|
+
revision: 5aeae33367fa324abf3a4ef912f4e27aaf17f6b9
|
|
134
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
135
|
+
repo_dir: gems
|
|
136
|
+
- name: rails-dom-testing
|
|
137
|
+
version: '2.0'
|
|
138
|
+
source:
|
|
139
|
+
type: git
|
|
140
|
+
name: ruby/gem_rbs_collection
|
|
141
|
+
revision: 5aeae33367fa324abf3a4ef912f4e27aaf17f6b9
|
|
142
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
143
|
+
repo_dir: gems
|
|
144
|
+
- name: rails-html-sanitizer
|
|
145
|
+
version: '1.6'
|
|
146
|
+
source:
|
|
147
|
+
type: git
|
|
148
|
+
name: ruby/gem_rbs_collection
|
|
149
|
+
revision: 5aeae33367fa324abf3a4ef912f4e27aaf17f6b9
|
|
150
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
151
|
+
repo_dir: gems
|
|
152
|
+
- name: railties
|
|
153
|
+
version: '6.0'
|
|
154
|
+
source:
|
|
155
|
+
type: git
|
|
156
|
+
name: ruby/gem_rbs_collection
|
|
157
|
+
revision: 5aeae33367fa324abf3a4ef912f4e27aaf17f6b9
|
|
158
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
159
|
+
repo_dir: gems
|
|
160
|
+
- name: rake
|
|
161
|
+
version: '13.0'
|
|
162
|
+
source:
|
|
163
|
+
type: git
|
|
164
|
+
name: ruby/gem_rbs_collection
|
|
165
|
+
revision: 5aeae33367fa324abf3a4ef912f4e27aaf17f6b9
|
|
166
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
167
|
+
repo_dir: gems
|
|
168
|
+
- name: rdoc
|
|
169
|
+
version: '0'
|
|
170
|
+
source:
|
|
171
|
+
type: stdlib
|
|
172
|
+
- name: securerandom
|
|
173
|
+
version: '0'
|
|
174
|
+
source:
|
|
175
|
+
type: stdlib
|
|
176
|
+
- name: singleton
|
|
177
|
+
version: '0'
|
|
178
|
+
source:
|
|
179
|
+
type: stdlib
|
|
180
|
+
- name: socket
|
|
181
|
+
version: '0'
|
|
182
|
+
source:
|
|
183
|
+
type: stdlib
|
|
184
|
+
- name: stringio
|
|
185
|
+
version: '0'
|
|
186
|
+
source:
|
|
187
|
+
type: stdlib
|
|
188
|
+
- name: tempfile
|
|
189
|
+
version: '0'
|
|
190
|
+
source:
|
|
191
|
+
type: stdlib
|
|
192
|
+
- name: thor
|
|
193
|
+
version: '1.2'
|
|
194
|
+
source:
|
|
195
|
+
type: git
|
|
196
|
+
name: ruby/gem_rbs_collection
|
|
197
|
+
revision: 5aeae33367fa324abf3a4ef912f4e27aaf17f6b9
|
|
198
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
199
|
+
repo_dir: gems
|
|
200
|
+
- name: time
|
|
201
|
+
version: '0'
|
|
202
|
+
source:
|
|
203
|
+
type: stdlib
|
|
204
|
+
- name: timeout
|
|
205
|
+
version: '0'
|
|
206
|
+
source:
|
|
207
|
+
type: stdlib
|
|
208
|
+
- name: tsort
|
|
209
|
+
version: '0'
|
|
210
|
+
source:
|
|
211
|
+
type: stdlib
|
|
212
|
+
- name: tzinfo
|
|
213
|
+
version: '2.0'
|
|
214
|
+
source:
|
|
215
|
+
type: git
|
|
216
|
+
name: ruby/gem_rbs_collection
|
|
217
|
+
revision: 5aeae33367fa324abf3a4ef912f4e27aaf17f6b9
|
|
218
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
219
|
+
repo_dir: gems
|
|
220
|
+
- name: uri
|
|
221
|
+
version: '0'
|
|
222
|
+
source:
|
|
223
|
+
type: stdlib
|
|
224
|
+
gemfile_lock_path: Gemfile.lock
|
data/rbs_collection.yaml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Download sources
|
|
2
|
+
sources:
|
|
3
|
+
- type: git
|
|
4
|
+
name: ruby/gem_rbs_collection
|
|
5
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
6
|
+
revision: main
|
|
7
|
+
repo_dir: gems
|
|
8
|
+
|
|
9
|
+
# You can specify local directories as sources also.
|
|
10
|
+
# - type: local
|
|
11
|
+
# path: path/to/your/local/repository
|
|
12
|
+
|
|
13
|
+
# A directory to install the downloaded RBSs
|
|
14
|
+
path: .gem_rbs_collection
|
|
15
|
+
|
|
16
|
+
# gems:
|
|
17
|
+
# # If you want to avoid installing rbs files for gems, you can specify them here.
|
|
18
|
+
# - name: GEM_NAME
|
|
19
|
+
# ignore: true
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Credentials
|
|
3
|
+
module Conflict
|
|
4
|
+
class EncryptionService
|
|
5
|
+
CIPHER: String
|
|
6
|
+
|
|
7
|
+
@key_path: (String | Pathname)
|
|
8
|
+
@env_key: String
|
|
9
|
+
@encryptor: ActiveSupport::MessageEncryptor?
|
|
10
|
+
|
|
11
|
+
def initialize: ((String | Pathname) key_path, ?env_key: String) -> void
|
|
12
|
+
|
|
13
|
+
def decrypt: (String? encrypted_content) -> String
|
|
14
|
+
|
|
15
|
+
def encrypt: (String content) -> String
|
|
16
|
+
|
|
17
|
+
def save_encrypted: (String content, (String | Pathname) destination_path) -> Integer
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def encryptor: () -> ActiveSupport::MessageEncryptor
|
|
22
|
+
|
|
23
|
+
def read_key: () -> String
|
|
24
|
+
|
|
25
|
+
def key_from_env: () -> String?
|
|
26
|
+
|
|
27
|
+
def key_from_file: () -> String?
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Credentials
|
|
3
|
+
module Conflict
|
|
4
|
+
class GitConflictHandler
|
|
5
|
+
type version = :base | :ours | :theirs
|
|
6
|
+
|
|
7
|
+
type merge_labels = { ours: String, base: String, theirs: String }
|
|
8
|
+
|
|
9
|
+
STAGE_BASE: Integer
|
|
10
|
+
STAGE_OURS: Integer
|
|
11
|
+
STAGE_THEIRS: Integer
|
|
12
|
+
|
|
13
|
+
@credentials_path: String
|
|
14
|
+
@relative_path: String
|
|
15
|
+
|
|
16
|
+
def initialize: ((String | Pathname) credentials_path, (String | Pathname) relative_path) -> void
|
|
17
|
+
|
|
18
|
+
def validate_conflict!: () -> void
|
|
19
|
+
|
|
20
|
+
def get_version: (version version) -> String
|
|
21
|
+
|
|
22
|
+
def stage_resolved_file!: () -> void
|
|
23
|
+
|
|
24
|
+
def get_merge_labels: () -> merge_labels
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def stage_for_version: (version version) -> Integer
|
|
29
|
+
|
|
30
|
+
def detect_head_ref: () -> String
|
|
31
|
+
|
|
32
|
+
def build_label: (String branch_name, String commit_sha) -> String
|
|
33
|
+
|
|
34
|
+
def get_current_branch: () -> String
|
|
35
|
+
|
|
36
|
+
def get_incoming_branch: (String head_ref) -> String
|
|
37
|
+
|
|
38
|
+
def get_short_sha: (String ref) -> String
|
|
39
|
+
|
|
40
|
+
def get_merge_base_sha: (String head_ref) -> String
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Credentials
|
|
3
|
+
module Conflict
|
|
4
|
+
class MergeStrategy
|
|
5
|
+
type merge_result = { content: String, has_conflicts: bool }
|
|
6
|
+
|
|
7
|
+
type merge_labels = { ours: String, base: String, theirs: String }
|
|
8
|
+
|
|
9
|
+
CONFLICT_MARKER_START: String
|
|
10
|
+
CONFLICT_MARKER_END: String
|
|
11
|
+
|
|
12
|
+
@encryption_service: EncryptionService
|
|
13
|
+
|
|
14
|
+
def initialize: (EncryptionService encryption_service) -> void
|
|
15
|
+
|
|
16
|
+
def create_conflict_markers: (
|
|
17
|
+
String ours_content,
|
|
18
|
+
String base_content,
|
|
19
|
+
String theirs_content,
|
|
20
|
+
labels: merge_labels
|
|
21
|
+
) -> merge_result
|
|
22
|
+
|
|
23
|
+
def has_conflict_markers?: (String content) -> bool
|
|
24
|
+
|
|
25
|
+
def validate_yaml!: (String content) -> void
|
|
26
|
+
|
|
27
|
+
def open_editor_for_resolution: (String merged_content) -> String
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def with_temp_files: (*String contents) { (*String paths) -> untyped } -> untyped
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Credentials
|
|
3
|
+
module Conflict
|
|
4
|
+
class PathResolver
|
|
5
|
+
attr_reader environment: String?
|
|
6
|
+
|
|
7
|
+
@environment: String?
|
|
8
|
+
|
|
9
|
+
def initialize: (?String? environment) -> void
|
|
10
|
+
|
|
11
|
+
def credentials_path: () -> Pathname
|
|
12
|
+
|
|
13
|
+
def key_path: () -> Pathname
|
|
14
|
+
|
|
15
|
+
def relative_credentials_path: () -> Pathname
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Credentials
|
|
3
|
+
module Conflict
|
|
4
|
+
class Resolver
|
|
5
|
+
attr_reader environment: String?
|
|
6
|
+
|
|
7
|
+
@environment: String?
|
|
8
|
+
@output: IO | StringIO
|
|
9
|
+
@path_resolver: PathResolver
|
|
10
|
+
@encryption_service: EncryptionService
|
|
11
|
+
@git_handler: GitConflictHandler
|
|
12
|
+
@merge_strategy: MergeStrategy
|
|
13
|
+
|
|
14
|
+
def initialize: (?String? environment, ?output: (IO | StringIO)) -> void
|
|
15
|
+
|
|
16
|
+
def resolve: () -> void
|
|
17
|
+
|
|
18
|
+
def yours: () -> void
|
|
19
|
+
|
|
20
|
+
def theirs: () -> void
|
|
21
|
+
|
|
22
|
+
def base: () -> void
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def log: (String message) -> void
|
|
27
|
+
|
|
28
|
+
def resolve_with_version: (GitConflictHandler::version version, String label) -> void
|
|
29
|
+
|
|
30
|
+
def decrypt_version: (GitConflictHandler::version version) -> String
|
|
31
|
+
|
|
32
|
+
def get_base_version: () -> String
|
|
33
|
+
|
|
34
|
+
def save_and_stage: (String content) -> void
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rails-credentials-conflict
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- jwo1f
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '6.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '6.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: railties
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '6.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '6.0'
|
|
40
|
+
description: A gem that helps resolve git merge conflicts in Rails encrypted credentials
|
|
41
|
+
by decrypting, merging, and re-encrypting the files.
|
|
42
|
+
email:
|
|
43
|
+
- ''
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- CHANGELOG.md
|
|
49
|
+
- LICENSE.txt
|
|
50
|
+
- README.md
|
|
51
|
+
- Rakefile
|
|
52
|
+
- lib/rails/credentials/conflict.rb
|
|
53
|
+
- lib/rails/credentials/conflict/encryption_service.rb
|
|
54
|
+
- lib/rails/credentials/conflict/git_conflict_handler.rb
|
|
55
|
+
- lib/rails/credentials/conflict/merge_strategy.rb
|
|
56
|
+
- lib/rails/credentials/conflict/path_resolver.rb
|
|
57
|
+
- lib/rails/credentials/conflict/railtie.rb
|
|
58
|
+
- lib/rails/credentials/conflict/resolver.rb
|
|
59
|
+
- lib/rails/credentials/conflict/version.rb
|
|
60
|
+
- lib/tasks/credentials_conflict.rake
|
|
61
|
+
- rbs_collection.lock.yaml
|
|
62
|
+
- rbs_collection.yaml
|
|
63
|
+
- sig/rails/credentials/conflict.rbs
|
|
64
|
+
- sig/rails/credentials/conflict/encryption_service.rbs
|
|
65
|
+
- sig/rails/credentials/conflict/git_conflict_handler.rbs
|
|
66
|
+
- sig/rails/credentials/conflict/merge_strategy.rbs
|
|
67
|
+
- sig/rails/credentials/conflict/path_resolver.rbs
|
|
68
|
+
- sig/rails/credentials/conflict/railtie.rbs
|
|
69
|
+
- sig/rails/credentials/conflict/resolver.rbs
|
|
70
|
+
homepage: https://github.com/jwo1f/rails-credentials-conflict
|
|
71
|
+
licenses:
|
|
72
|
+
- MIT
|
|
73
|
+
metadata:
|
|
74
|
+
homepage_uri: https://github.com/jwo1f/rails-credentials-conflict
|
|
75
|
+
source_code_uri: https://github.com/jwo1f/rails-credentials-conflict
|
|
76
|
+
changelog_uri: https://github.com/jwo1f/rails-credentials-conflict/blob/main/CHANGELOG.md
|
|
77
|
+
rubygems_mfa_required: 'true'
|
|
78
|
+
rdoc_options: []
|
|
79
|
+
require_paths:
|
|
80
|
+
- lib
|
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
82
|
+
requirements:
|
|
83
|
+
- - ">="
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: 3.1.0
|
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - ">="
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '0'
|
|
91
|
+
requirements: []
|
|
92
|
+
rubygems_version: 4.0.0
|
|
93
|
+
specification_version: 4
|
|
94
|
+
summary: Resolve Rails encrypted credentials conflicts during git merges
|
|
95
|
+
test_files: []
|