git_lib 1.2.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/.dependabot/config.yml +10 -0
- data/.github/workflows/gem_release.yml +37 -0
- data/.gitignore +53 -0
- data/.jenkins/Jenkinsfile +72 -0
- data/.jenkins/ruby_build_pod.yml +19 -0
- data/.rspec +3 -0
- data/.rubocop.yml +43 -0
- data/.ruby-version +1 -0
- data/Appraisals +13 -0
- data/CHANGELOG.md +24 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +121 -0
- data/README.md +9 -0
- data/Rakefile +17 -0
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/rails_4.gemfile +21 -0
- data/gemfiles/rails_5.gemfile +21 -0
- data/gemfiles/rails_6.gemfile +21 -0
- data/git_lib.gemspec +25 -0
- data/lib/git/git.rb +186 -0
- data/lib/git/git_branch.rb +35 -0
- data/lib/git/git_commit.rb +23 -0
- data/lib/git/git_conflict.rb +29 -0
- data/lib/git/git_error.rb +14 -0
- data/lib/git/git_test_helpers.rb +38 -0
- data/lib/git/version.rb +5 -0
- data/lib/git_lib.rb +7 -0
- data/spec/lib/git/git_branch_spec.rb +57 -0
- data/spec/lib/git/git_conflict_spec.rb +50 -0
- data/spec/lib/git/git_error_spec.rb +19 -0
- data/spec/lib/git/git_spec.rb +436 -0
- data/spec/spec_helper.rb +30 -0
- metadata +100 -0
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
require 'rubocop/rake_task'
|
6
|
+
require 'coveralls/rake/task'
|
7
|
+
require "bundler/gem_tasks"
|
8
|
+
require 'bundler/audit/task'
|
9
|
+
|
10
|
+
RSpec::Core::RakeTask.new(:spec)
|
11
|
+
RuboCop::RakeTask.new
|
12
|
+
Coveralls::RakeTask.new
|
13
|
+
Bundler::Audit::Task.new
|
14
|
+
|
15
|
+
task test: :spec
|
16
|
+
task rspec: :spec
|
17
|
+
task default: :spec
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "activesupport", "~> 4.2"
|
6
|
+
gem "appraisal"
|
7
|
+
gem "bundler", "~> 1.17"
|
8
|
+
gem "bundler-audit", require: false
|
9
|
+
gem "climate_control", "~> 0.2"
|
10
|
+
gem "coveralls", require: false
|
11
|
+
gem "fakefs", "~> 0.9", require: "fakefs/safe"
|
12
|
+
gem "json", "~> 1.8"
|
13
|
+
gem "pry"
|
14
|
+
gem "pry-byebug"
|
15
|
+
gem "rake"
|
16
|
+
gem "rspec", "~> 3.5"
|
17
|
+
gem "rspec_junit_formatter"
|
18
|
+
gem "rubocop", require: false
|
19
|
+
gem "rubocop-rspec", require: false
|
20
|
+
|
21
|
+
gemspec path: "../"
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "activesupport", "~> 5.2"
|
6
|
+
gem "appraisal"
|
7
|
+
gem "bundler", "~> 1.17"
|
8
|
+
gem "bundler-audit", require: false
|
9
|
+
gem "climate_control", "~> 0.2"
|
10
|
+
gem "coveralls", require: false
|
11
|
+
gem "fakefs", "~> 0.9", require: "fakefs/safe"
|
12
|
+
gem "json", "~> 1.8"
|
13
|
+
gem "pry"
|
14
|
+
gem "pry-byebug"
|
15
|
+
gem "rake"
|
16
|
+
gem "rspec", "~> 3.5"
|
17
|
+
gem "rspec_junit_formatter"
|
18
|
+
gem "rubocop", require: false
|
19
|
+
gem "rubocop-rspec", require: false
|
20
|
+
|
21
|
+
gemspec path: "../"
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "activesupport", "~> 6.0"
|
6
|
+
gem "appraisal"
|
7
|
+
gem "bundler", "~> 1.17"
|
8
|
+
gem "bundler-audit", require: false
|
9
|
+
gem "climate_control", "~> 0.2"
|
10
|
+
gem "coveralls", require: false
|
11
|
+
gem "fakefs", "~> 0.9", require: "fakefs/safe"
|
12
|
+
gem "json", "~> 1.8"
|
13
|
+
gem "pry"
|
14
|
+
gem "pry-byebug"
|
15
|
+
gem "rake"
|
16
|
+
gem "rspec", "~> 3.5"
|
17
|
+
gem "rspec_junit_formatter"
|
18
|
+
gem "rubocop", require: false
|
19
|
+
gem "rubocop-rspec", require: false
|
20
|
+
|
21
|
+
gemspec path: "../"
|
data/git_lib.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'git/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "git_lib"
|
9
|
+
spec.version = Git::VERSION
|
10
|
+
spec.authors = ["Invoca Development"]
|
11
|
+
spec.email = ["development@invoca.com"]
|
12
|
+
spec.summary = "Git wrapper library."
|
13
|
+
spec.homepage = "https://github.com/Invoca/git_lib"
|
14
|
+
|
15
|
+
spec.metadata = {
|
16
|
+
'allowed_push_host' => "https://rubygems.org"
|
17
|
+
}
|
18
|
+
|
19
|
+
spec.files = `git ls-files -z`.split("\x0")
|
20
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
21
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.add_dependency 'activesupport', '>= 4.2', '< 7'
|
25
|
+
end
|
data/lib/git/git.rb
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
require 'shellwords'
|
5
|
+
require 'active_support/core_ext/object/blank'
|
6
|
+
require 'active_support/core_ext/time/calculations'
|
7
|
+
require_relative './git_branch.rb'
|
8
|
+
require_relative './git_commit.rb'
|
9
|
+
require_relative './git_conflict.rb'
|
10
|
+
require_relative './git_error.rb'
|
11
|
+
|
12
|
+
module Git
|
13
|
+
class Git
|
14
|
+
GIT_PATH = '/usr/bin/git'.freeze
|
15
|
+
|
16
|
+
attr_reader :repository_name, :repository_url, :repository_path
|
17
|
+
|
18
|
+
def initialize(repository_name, git_cache_path: '/tmp/git')
|
19
|
+
@repository_name = repository_name
|
20
|
+
@repository_url = "git@github.com:#{repository_name}.git"
|
21
|
+
@repository_path = File.join(git_cache_path, repository_name).to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
def execute(command, run_in_repository_path = true)
|
25
|
+
options = if run_in_repository_path
|
26
|
+
{ chdir: @repository_path }
|
27
|
+
else
|
28
|
+
{}
|
29
|
+
end
|
30
|
+
stdout_andstderr_str, status = Open3.capture2e(GIT_PATH, *command.split(/ /), options)
|
31
|
+
unless status.success?
|
32
|
+
raise GitError.new("#{GIT_PATH} #{command}", status, stdout_andstderr_str)
|
33
|
+
end
|
34
|
+
|
35
|
+
stdout_andstderr_str
|
36
|
+
end
|
37
|
+
|
38
|
+
def branch_list
|
39
|
+
raw_output = execute(
|
40
|
+
'for-each-ref refs/remotes/ --format=\'%(refname:short)~%(authordate:iso8601)~%(authorname)~%(authoremail)\''
|
41
|
+
)
|
42
|
+
|
43
|
+
raw_output.split("\n").collect! do |raw_branch_data|
|
44
|
+
branch_data = raw_branch_data.split('~')
|
45
|
+
GitBranch.new(
|
46
|
+
@repository_name,
|
47
|
+
branch_data[0].sub!('origin/', ''),
|
48
|
+
DateTime.parse(branch_data[1]),
|
49
|
+
branch_data[2],
|
50
|
+
branch_data[3].gsub!(/[<>]/, '')
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def merge_branches(target_branch_name,
|
56
|
+
source_branch_name,
|
57
|
+
source_tag_name: nil,
|
58
|
+
keep_changes: true,
|
59
|
+
commit_message: nil)
|
60
|
+
if current_branch_name != target_branch_name
|
61
|
+
checkout_branch(target_branch_name)
|
62
|
+
end
|
63
|
+
|
64
|
+
commit_message_argument = "-m \"#{commit_message.gsub('"', '\\"')}\"" if commit_message
|
65
|
+
source = if source_tag_name.present?
|
66
|
+
Shellwords.escape(source_tag_name)
|
67
|
+
else
|
68
|
+
"origin/#{Shellwords.escape(source_branch_name)}"
|
69
|
+
end
|
70
|
+
|
71
|
+
raw_output = execute("merge --no-edit #{commit_message_argument} #{source}")
|
72
|
+
|
73
|
+
if raw_output =~ /.*Already up-to-date.\n/
|
74
|
+
[false, nil]
|
75
|
+
else
|
76
|
+
[true, nil]
|
77
|
+
end
|
78
|
+
rescue GitError => ex
|
79
|
+
conflicting_files = Git.get_conflict_list_from_failed_merge_output(ex.error_message)
|
80
|
+
if conflicting_files.empty?
|
81
|
+
raise
|
82
|
+
else
|
83
|
+
[
|
84
|
+
false,
|
85
|
+
GitConflict.new(
|
86
|
+
@repository_name,
|
87
|
+
target_branch_name,
|
88
|
+
source_branch_name,
|
89
|
+
conflicting_files
|
90
|
+
)
|
91
|
+
]
|
92
|
+
end
|
93
|
+
ensure
|
94
|
+
# cleanup our "mess"
|
95
|
+
keep_changes || reset
|
96
|
+
end
|
97
|
+
|
98
|
+
def clone_repository(default_branch_name)
|
99
|
+
if Dir.exist?(@repository_path)
|
100
|
+
# cleanup any changes that might have been left over if we crashed while running
|
101
|
+
reset
|
102
|
+
execute('clean -f -d')
|
103
|
+
|
104
|
+
# move to the master branch
|
105
|
+
checkout_branch(default_branch_name)
|
106
|
+
|
107
|
+
# remove branches that no longer exist on origin and update all branches that do
|
108
|
+
execute('fetch --prune --all')
|
109
|
+
|
110
|
+
# pull all of the branches
|
111
|
+
execute('pull --all')
|
112
|
+
else
|
113
|
+
execute("clone #{@repository_url} #{@repository_path}", false)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def push(dry_run: false)
|
118
|
+
dry_run_argument = ''
|
119
|
+
if dry_run
|
120
|
+
dry_run_argument = '--dry-run'
|
121
|
+
end
|
122
|
+
raw_output = execute("push #{dry_run_argument} origin")
|
123
|
+
raw_output != "Everything up-to-date\n"
|
124
|
+
end
|
125
|
+
|
126
|
+
def checkout_branch(branch_name)
|
127
|
+
reset
|
128
|
+
execute("checkout #{Shellwords.escape(branch_name)}")
|
129
|
+
reset
|
130
|
+
end
|
131
|
+
|
132
|
+
def reset
|
133
|
+
execute("reset --hard origin/#{Shellwords.escape(current_branch_name)}")
|
134
|
+
end
|
135
|
+
|
136
|
+
def lookup_tag(tag)
|
137
|
+
execute("describe --abbrev=0 --match #{tag}").strip
|
138
|
+
end
|
139
|
+
|
140
|
+
def fetch_all
|
141
|
+
execute('fetch --all')
|
142
|
+
end
|
143
|
+
|
144
|
+
def file_diff_branch_with_ancestor(branch_name, ancestor_branch_name)
|
145
|
+
# gets the merge base of the branch and its ancestor, then gets a list of files changed since the merge base
|
146
|
+
raw_output = execute(
|
147
|
+
"diff --name-only $(git merge-base origin/#{Shellwords.escape(ancestor_branch_name)} " \
|
148
|
+
"origin/#{Shellwords.escape(branch_name)})..origin/#{Shellwords.escape(branch_name)}"
|
149
|
+
)
|
150
|
+
raw_output.split("\n")
|
151
|
+
end
|
152
|
+
|
153
|
+
def commit_diff_refs(ref, ancestor_ref, fetch: false)
|
154
|
+
if fetch
|
155
|
+
fetch_all
|
156
|
+
end
|
157
|
+
ref_prefix = 'origin/' unless self.class.is_git_sha?(ref)
|
158
|
+
ancestor_ref_prefix = 'origin/' unless self.class.is_git_sha?(ancestor_ref)
|
159
|
+
|
160
|
+
raw_output = execute(
|
161
|
+
"log --format=%H\t%an\t%ae\t%aI\t%s " \
|
162
|
+
"--no-color #{ancestor_ref_prefix}#{Shellwords.escape(ancestor_ref)}..#{ref_prefix}#{Shellwords.escape(ref)}"
|
163
|
+
)
|
164
|
+
raw_output.split("\n").map do |row|
|
165
|
+
commit_data = row.split("\t")
|
166
|
+
GitCommit.new(commit_data[0], commit_data[4], DateTime.iso8601(commit_data[3]), commit_data[1], commit_data[2])
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def current_branch_name
|
171
|
+
execute('rev-parse --abbrev-ref HEAD').strip
|
172
|
+
end
|
173
|
+
|
174
|
+
class << self
|
175
|
+
def is_git_sha?(str) # rubocop:disable Style/PredicateName
|
176
|
+
(str =~ /[0-9a-f]{40}/) == 0
|
177
|
+
end
|
178
|
+
|
179
|
+
def get_conflict_list_from_failed_merge_output(failed_merged_output)
|
180
|
+
failed_merged_output.split("\n").grep(/CONFLICT/).collect! do |conflict|
|
181
|
+
conflict.sub(/CONFLICT \(.*\): /, '').sub(/Merge conflict in /, '').sub(/ deleted in .*/, '')
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Git
|
4
|
+
class GitBranch
|
5
|
+
attr_reader :repository_name, :name, :last_modified_date, :author_name, :author_email
|
6
|
+
|
7
|
+
def initialize(repository_name, name, last_modified_date, author_name, author_email)
|
8
|
+
@repository_name = repository_name
|
9
|
+
@name = name
|
10
|
+
@last_modified_date = last_modified_date
|
11
|
+
@author_email = author_email
|
12
|
+
@author_name = author_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
name
|
17
|
+
end
|
18
|
+
|
19
|
+
def =~(other)
|
20
|
+
name =~ other
|
21
|
+
end
|
22
|
+
|
23
|
+
def ==(other)
|
24
|
+
repository_name == other.repository_name \
|
25
|
+
&& name == other.name && \
|
26
|
+
last_modified_date == other.last_modified_date \
|
27
|
+
&& author_email == other.author_email \
|
28
|
+
&& author_name == other.author_name
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.name_from_ref(ref)
|
32
|
+
ref.gsub(/^refs\/heads\//, '')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Git
|
4
|
+
class GitCommit
|
5
|
+
attr_reader :sha, :message, :commit_date, :author_name, :author_email
|
6
|
+
|
7
|
+
def initialize(sha, message, commit_date, author_name, author_email)
|
8
|
+
@sha = sha
|
9
|
+
@message = message
|
10
|
+
@commit_date = commit_date
|
11
|
+
@author_email = author_email
|
12
|
+
@author_name = author_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
sha
|
17
|
+
end
|
18
|
+
|
19
|
+
def ==(other)
|
20
|
+
sha == other.sha
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Git
|
4
|
+
class GitConflict
|
5
|
+
attr_reader :repository_name, :branch_a, :branch_b, :conflicting_files
|
6
|
+
|
7
|
+
def initialize(repository_name, branch_a, branch_b, conflicting_files)
|
8
|
+
unless conflicting_files.present?
|
9
|
+
raise ArgumentError, 'Must specify conflicting file list'
|
10
|
+
end
|
11
|
+
|
12
|
+
@repository_name = repository_name
|
13
|
+
@branch_a = branch_a
|
14
|
+
@branch_b = branch_b
|
15
|
+
@conflicting_files = conflicting_files
|
16
|
+
end
|
17
|
+
|
18
|
+
def ==(other)
|
19
|
+
repository_name == other.repository_name \
|
20
|
+
&& branch_a == other.branch_a \
|
21
|
+
&& branch_b == other.branch_b \
|
22
|
+
&& conflicting_files == other.conflicting_files
|
23
|
+
end
|
24
|
+
|
25
|
+
def contains_branch(branch_name)
|
26
|
+
branch_a == branch_name || branch_b == branch_name
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Git
|
4
|
+
class GitError < StandardError
|
5
|
+
attr_reader :command, :exit_code, :error_message
|
6
|
+
|
7
|
+
def initialize(command, exit_code, error_message)
|
8
|
+
@command = command
|
9
|
+
@exit_code = exit_code
|
10
|
+
@error_message = error_message
|
11
|
+
super("Git command #{@command} failed with exit code #{@exit_code}. Message:\n#{@error_message}")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest/sha1'
|
4
|
+
require 'securerandom'
|
5
|
+
require_relative './git_branch.rb'
|
6
|
+
require_relative './git_conflict.rb'
|
7
|
+
require_relative './git_commit.rb'
|
8
|
+
|
9
|
+
module Git
|
10
|
+
module TestHelpers
|
11
|
+
def self.create_branch(repository_name: 'repository_name',
|
12
|
+
name: 'path/branch',
|
13
|
+
last_modified_date: Time.current,
|
14
|
+
author_name: 'Author Name',
|
15
|
+
author_email: 'author@email.com')
|
16
|
+
GitBranch.new(repository_name, name, last_modified_date, author_name, author_email)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.create_conflict(repository_name: 'repository_name',
|
20
|
+
branch_a_name: 'branch_a',
|
21
|
+
branch_b_name: 'branch_b',
|
22
|
+
file_list: ['file1', 'file2'])
|
23
|
+
GitConflict.new(repository_name, branch_a_name, branch_b_name, file_list)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.create_commit(sha: '1234567890123456789012345678901234567890',
|
27
|
+
message: 'Commit message',
|
28
|
+
author_name: 'Author Name',
|
29
|
+
author_email: 'author@email.com',
|
30
|
+
commit_date: Time.current)
|
31
|
+
GitCommit.new(sha, message, commit_date, author_name, author_email)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.create_sha
|
35
|
+
Digest::SHA1.hexdigest(SecureRandom.hex)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|