git_lib 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|