summaryrefslogtreecommitdiff
path: root/qa/qa/git/repository.rb
blob: eb582aa6e47862e267794bf937d0857b04d2f46f (plain)
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
# frozen_string_literal: true

require 'cgi'
require 'uri'
require 'open3'
require 'fileutils'
require 'tmpdir'

module QA
  module Git
    class Repository
      include Scenario::Actable

      attr_writer :password, :use_lfs
      attr_accessor :env_vars

      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 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)
        ::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?

        # 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

      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?
        return true if ssh_key_set? && use_lfs?

        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}]"

        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 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?
        File.exist?(netrc_file_path) &&
          File.readlines(netrc_file_path).grep(/^#{netrc_content}$/).any?
      end
    end
  end
end