1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
|
# frozen_string_literal: true
require 'cgi'
require 'uri'
require 'open3'
require 'fileutils'
require 'tmpdir'
require 'tempfile'
require 'securerandom'
module QA
module Git
class Repository
include Scenario::Actable
RepositoryCommandError = Class.new(StandardError)
attr_writer :use_lfs
attr_accessor :env_vars
InvalidCredentialsError = Class.new(RuntimeError)
def initialize
# We set HOME to the current working directory (which is a
# temporary directory created in .perform()) so the temporarily dropped
# .netrc can be utilised
self.env_vars = [%Q{HOME="#{tmp_home_dir}"}]
@use_lfs = false
end
def self.perform(*args)
Dir.mktmpdir do |dir|
Dir.chdir(dir) { super }
end
end
def password=(password)
@password = password
raise InvalidCredentialsError, "Please provide a username when setting a password" unless username
try_add_credentials_to_netrc
end
def uri=(address)
@uri = URI(address)
end
def username=(username)
@username = username
# Only include the user in the URI if we're using HTTP as this breaks
# SSH authentication.
@uri.user = username unless ssh_key_set?
end
def use_default_credentials
self.username, self.password = default_credentials
end
def clone(opts = '')
clone_result = run("git clone #{opts} #{uri} ./")
return clone_result.response unless clone_result.success
enable_lfs_result = enable_lfs if use_lfs?
clone_result.to_s + enable_lfs_result.to_s
end
def checkout(branch_name, new_branch: false)
opts = new_branch ? '-b' : ''
run(%Q{git checkout #{opts} "#{branch_name}"}).to_s
end
def shallow_clone
clone('--depth 1')
end
def configure_identity(name, email)
run(%Q{git config user.name #{name}})
run(%Q{git config user.email #{email}})
end
def commit_file(name, contents, message)
add_file(name, contents)
commit(message)
end
def add_file(name, contents)
FileUtils.mkdir_p(::File.dirname(name))
::File.write(name, contents)
if use_lfs?
git_lfs_track_result = run(%Q{git lfs track #{name} --lockable})
return git_lfs_track_result.response unless git_lfs_track_result.success
end
git_add_result = run(%Q{git add #{name}})
git_lfs_track_result.to_s + git_add_result.to_s
end
def commit(message)
run(%Q{git commit -m "#{message}"}).to_s
end
def push_changes(branch = 'master')
run("git push #{uri} #{branch}").to_s
end
def merge(branch)
run("git merge #{branch}")
end
def commits
run('git log --oneline').to_s.split("\n")
end
def use_ssh_key(key)
@private_key_file = Tempfile.new("id_#{SecureRandom.hex(8)}")
File.binwrite(private_key_file, key.private_key)
File.chmod(0700, private_key_file)
@known_hosts_file = Tempfile.new("known_hosts_#{SecureRandom.hex(8)}")
keyscan_params = ['-H']
keyscan_params << "-p #{uri.port}" if uri.port
keyscan_params << uri.host
res = run("ssh-keyscan #{keyscan_params.join(' ')} >> #{known_hosts_file.path}")
return res.response unless res.success?
self.env_vars << %Q{GIT_SSH_COMMAND="ssh -i #{private_key_file.path} -o UserKnownHostsFile=#{known_hosts_file.path}"}
end
def delete_ssh_key
return unless ssh_key_set?
private_key_file.close(true)
known_hosts_file.close(true)
end
def push_with_git_protocol(version, file_name, file_content, commit_message = 'Initial commit')
self.git_protocol = version
add_file(file_name, file_content)
commit(commit_message)
push_changes
fetch_supported_git_protocol
end
def git_protocol=(value)
raise ArgumentError, "Please specify the protocol you would like to use: 0, 1, or 2" unless %w[0 1 2].include?(value.to_s)
run("git config protocol.version #{value}")
end
def fetch_supported_git_protocol
# ls-remote is one command known to respond to Git protocol v2 so we use
# it to get output including the version reported via Git tracing
output = run("git ls-remote #{uri}", "GIT_TRACE_PACKET=1")
output[/git< version (\d+)/, 1] || 'unknown'
end
def try_add_credentials_to_netrc
return unless add_credentials?
return if netrc_already_contains_content?
save_netrc_content
end
private
attr_reader :uri, :username, :password, :known_hosts_file,
:private_key_file, :use_lfs
alias_method :use_lfs?, :use_lfs
Result = Struct.new(:success, :response) do
alias_method :success?, :success
alias_method :to_s, :response
end
def add_credentials?
return false if !username || !password
return true unless ssh_key_set?
false
end
def ssh_key_set?
!private_key_file.nil?
end
def enable_lfs
# git lfs install *needs* a .gitconfig defined at ${HOME}/.gitconfig
FileUtils.mkdir_p(tmp_home_dir)
touch_gitconfig_result = run("touch #{tmp_home_dir}/.gitconfig")
return touch_gitconfig_result.response unless touch_gitconfig_result.success?
git_lfs_install_result = run('git lfs install')
touch_gitconfig_result.to_s + git_lfs_install_result.to_s
end
def run(command_str, *extra_env)
command = [env_vars, *extra_env, command_str, '2>&1'].compact.join(' ')
Runtime::Logger.debug "Git: pwd=[#{Dir.pwd}], command=[#{command}]"
output, status = Open3.capture2e(command)
output.chomp!
Runtime::Logger.debug "Git: output=[#{output}], exitstatus=[#{status.exitstatus}]"
unless status.success?
raise RepositoryCommandError, "The command #{command} failed (#{status.exitstatus}) with the following output:\n#{output}"
end
Result.new(status.exitstatus == 0, output)
end
def default_credentials
if ::QA::Runtime::User.ldap_user?
[Runtime::User.ldap_username, Runtime::User.ldap_password]
else
[Runtime::User.username, Runtime::User.password]
end
end
def read_netrc_content
File.exist?(netrc_file_path) ? File.readlines(netrc_file_path) : []
end
def save_netrc_content
# Despite libcurl supporting a custom .netrc location through the
# CURLOPT_NETRC_FILE environment variable, git does not support it :(
# Info: https://curl.haxx.se/libcurl/c/CURLOPT_NETRC_FILE.html
#
# This will create a .netrc in the correct working directory, which is
# a temporary directory created in .perform()
#
FileUtils.mkdir_p(tmp_home_dir)
File.open(netrc_file_path, 'a') { |file| file.puts(netrc_content) }
File.chmod(0600, netrc_file_path)
end
def tmp_home_dir
@tmp_home_dir ||= File.join(Dir.tmpdir, "qa-netrc-credentials", $$.to_s)
end
def netrc_file_path
@netrc_file_path ||= File.join(tmp_home_dir, '.netrc')
end
def netrc_content
"machine #{uri.host} login #{username} password #{password}"
end
def netrc_already_contains_content?
read_netrc_content.grep(/^#{Regexp.escape(netrc_content)}$/).any?
end
end
end
end
|