git_lib 1.2.0

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