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
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
|
# 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
include Support::Repeater
RepositoryCommandError = Class.new(StandardError)
attr_writer :use_lfs, :gpg_key_id
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
@gpg_key_id = nil
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 use_default_identity
configure_identity('GitLab QA', 'root@gitlab.com')
end
def clone(opts = '')
clone_result = run("git clone #{opts} #{uri} ./", max_attempts: 3)
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 add_tag(tag_name)
run("git tag #{tag_name}").to_s
end
def delete_tag(tag_name)
run(%Q{git push origin --delete #{tag_name}}, max_attempts: 3).to_s
end
def commit(message)
run(%Q{git commit -m "#{message}"}, max_attempts: 3).to_s
end
def commit_with_gpg(message)
run(%Q{git config user.signingkey #{@gpg_key_id} && git config gpg.program $(command -v gpg) && git commit -S -m "#{message}"}).to_s
end
def current_branch
run("git rev-parse --abbrev-ref HEAD").to_s
end
def push_changes(branch = 'master', push_options: nil)
cmd = ['git push']
cmd << push_options_hash_to_string(push_options)
cmd << uri
cmd << branch
run(cmd.compact.join(' '), max_attempts: 3).to_s
end
def push_all_branches
run("git push --all").to_s
end
def push_tags_and_branches(branches)
run("git push --tags origin #{branches.join(' ')}").to_s
end
def merge(branch)
run("git merge #{branch}")
end
def init_repository
run("git init")
end
def pull(repository = nil, branch = nil)
run(['git', 'pull', repository, branch].compact.join(' '))
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
result = run("git ls-remote #{uri}", env: "GIT_TRACE_PACKET=1", max_attempts: 3)
result.response[/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
def file_content(file)
run("cat #{file}").to_s
end
def delete_netrc
File.delete(netrc_file_path) if File.exist?(netrc_file_path)
end
private
attr_reader :uri, :username, :password, :known_hosts_file,
:private_key_file, :use_lfs
alias_method :use_lfs?, :use_lfs
Result = Struct.new(:command, :exitstatus, :response) do
alias_method :to_s, :response
def success?
exitstatus == 0 && !response.include?('Error encountered')
end
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, env: [], max_attempts: 1)
command = [env_vars, *env, command_str, '2>&1'].compact.join(' ')
result = nil
repeat_until(max_attempts: max_attempts, raise_on_failure: false) do
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}]"
result = Result.new(command, status.exitstatus, output)
result.success?
end
unless result.success?
raise RepositoryCommandError, "The command #{result.command} failed (#{result.exitstatus}) with the following output:\n#{result.response}"
end
result
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 push_options_hash_to_string(opts)
return if opts.nil?
prefix = "-o merge_request"
opts.each_with_object([]) do |(key, value), options|
if value.is_a?(Array)
value.each do |item|
options << "#{prefix}.#{key}=\"#{item}\""
end
elsif value == true
options << "#{prefix}.#{key}"
else
options << "#{prefix}.#{key}=\"#{value}\""
end
end.join(' ')
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
|