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.
@@ -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,2 @@
1
+ ---
2
+ BUNDLE_RETRY: "1"
@@ -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: "../"
@@ -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
@@ -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