1# frozen_string_literal: true
2
3module Gitlab
4  module GitalyClient
5    class RepositoryService
6      include Gitlab::EncodingHelper
7
8      MAX_MSG_SIZE = 128.kilobytes
9
10      def initialize(repository)
11        @repository = repository
12        @gitaly_repo = repository.gitaly_repository
13        @storage = repository.storage
14      end
15
16      def exists?
17        request = Gitaly::RepositoryExistsRequest.new(repository: @gitaly_repo)
18
19        response = GitalyClient.call(@storage, :repository_service, :repository_exists, request, timeout: GitalyClient.fast_timeout)
20
21        response.exists
22      end
23
24      def cleanup
25        request = Gitaly::CleanupRequest.new(repository: @gitaly_repo)
26        GitalyClient.call(@storage, :repository_service, :cleanup, request, timeout: GitalyClient.fast_timeout)
27      end
28
29      def garbage_collect(create_bitmap, prune:)
30        request = Gitaly::GarbageCollectRequest.new(repository: @gitaly_repo, create_bitmap: create_bitmap, prune: prune)
31        GitalyClient.call(@storage, :repository_service, :garbage_collect, request, timeout: GitalyClient.long_timeout)
32      end
33
34      def repack_full(create_bitmap)
35        request = Gitaly::RepackFullRequest.new(repository: @gitaly_repo, create_bitmap: create_bitmap)
36        GitalyClient.call(@storage, :repository_service, :repack_full, request, timeout: GitalyClient.long_timeout)
37      end
38
39      def repack_incremental
40        request = Gitaly::RepackIncrementalRequest.new(repository: @gitaly_repo)
41        GitalyClient.call(@storage, :repository_service, :repack_incremental, request, timeout: GitalyClient.long_timeout)
42      end
43
44      def repository_size
45        request = Gitaly::RepositorySizeRequest.new(repository: @gitaly_repo)
46        response = GitalyClient.call(@storage, :repository_service, :repository_size, request, timeout: GitalyClient.medium_timeout)
47        response.size
48      end
49
50      def get_object_directory_size
51        request = Gitaly::GetObjectDirectorySizeRequest.new(repository: @gitaly_repo)
52        response = GitalyClient.call(@storage, :repository_service, :get_object_directory_size, request, timeout: GitalyClient.medium_timeout)
53
54        response.size
55      end
56
57      def apply_gitattributes(revision)
58        request = Gitaly::ApplyGitattributesRequest.new(repository: @gitaly_repo, revision: encode_binary(revision))
59        GitalyClient.call(@storage, :repository_service, :apply_gitattributes, request, timeout: GitalyClient.fast_timeout)
60      rescue GRPC::InvalidArgument => ex
61        raise Gitlab::Git::Repository::InvalidRef, ex
62      end
63
64      def info_attributes
65        request = Gitaly::GetInfoAttributesRequest.new(repository: @gitaly_repo)
66
67        response = GitalyClient.call(@storage, :repository_service, :get_info_attributes, request, timeout: GitalyClient.fast_timeout)
68        response.each_with_object([]) do |message, attributes|
69          attributes << message.attributes
70        end.join
71      end
72
73      # rubocop: disable Metrics/ParameterLists
74      # The `remote` parameter is going away soonish anyway, at which point the
75      # Rubocop warning can be enabled again.
76      def fetch_remote(url, refmap:, ssh_auth:, forced:, no_tags:, timeout:, prune: true, check_tags_changed: false, http_authorization_header: "")
77        request = Gitaly::FetchRemoteRequest.new(
78          repository: @gitaly_repo,
79          force: forced,
80          no_tags: no_tags,
81          timeout: timeout,
82          no_prune: !prune,
83          check_tags_changed: check_tags_changed,
84          remote_params: Gitaly::Remote.new(
85            url: url,
86            mirror_refmaps: Array.wrap(refmap).map(&:to_s),
87            http_authorization_header: http_authorization_header
88          )
89        )
90
91        if ssh_auth&.ssh_mirror_url?
92          if ssh_auth.ssh_key_auth? && ssh_auth.ssh_private_key.present?
93            request.ssh_key = ssh_auth.ssh_private_key
94          end
95
96          if ssh_auth.ssh_known_hosts.present?
97            request.known_hosts = ssh_auth.ssh_known_hosts
98          end
99        end
100
101        GitalyClient.call(@storage, :repository_service, :fetch_remote, request, timeout: GitalyClient.long_timeout)
102      end
103      # rubocop: enable Metrics/ParameterLists
104
105      def create_repository
106        request = Gitaly::CreateRepositoryRequest.new(repository: @gitaly_repo)
107        GitalyClient.call(@storage, :repository_service, :create_repository, request, timeout: GitalyClient.fast_timeout)
108      end
109
110      def has_local_branches?
111        request = Gitaly::HasLocalBranchesRequest.new(repository: @gitaly_repo)
112        response = GitalyClient.call(@storage, :repository_service, :has_local_branches, request, timeout: GitalyClient.fast_timeout)
113
114        response.value
115      end
116
117      def find_merge_base(*revisions)
118        request = Gitaly::FindMergeBaseRequest.new(
119          repository: @gitaly_repo,
120          revisions: revisions.map { |r| encode_binary(r) }
121        )
122
123        response = GitalyClient.call(@storage, :repository_service, :find_merge_base, request, timeout: GitalyClient.fast_timeout)
124        response.base.presence
125      end
126
127      def fork_repository(source_repository)
128        request = Gitaly::CreateForkRequest.new(
129          repository: @gitaly_repo,
130          source_repository: source_repository.gitaly_repository
131        )
132
133        GitalyClient.call(
134          @storage,
135          :repository_service,
136          :create_fork,
137          request,
138          remote_storage: source_repository.storage,
139          timeout: GitalyClient.long_timeout
140        )
141      end
142
143      def import_repository(source)
144        request = Gitaly::CreateRepositoryFromURLRequest.new(
145          repository: @gitaly_repo,
146          url: source
147        )
148
149        GitalyClient.call(
150          @storage,
151          :repository_service,
152          :create_repository_from_url,
153          request,
154          timeout: GitalyClient.long_timeout
155        )
156      end
157
158      def fetch_source_branch(source_repository, source_branch, local_ref)
159        request = Gitaly::FetchSourceBranchRequest.new(
160          repository: @gitaly_repo,
161          source_repository: source_repository.gitaly_repository,
162          source_branch: source_branch.b,
163          target_ref: local_ref.b
164        )
165
166        response = GitalyClient.call(
167          @storage,
168          :repository_service,
169          :fetch_source_branch,
170          request,
171          timeout: GitalyClient.long_timeout,
172          remote_storage: source_repository.storage
173        )
174
175        response.result
176      end
177
178      def fsck
179        request = Gitaly::FsckRequest.new(repository: @gitaly_repo)
180        response = GitalyClient.call(@storage, :repository_service, :fsck, request, timeout: GitalyClient.long_timeout)
181
182        if response.error.empty?
183          ["", 0]
184        else
185          [response.error.b, 1]
186        end
187      end
188
189      def create_bundle(save_path)
190        gitaly_fetch_stream_to_file(
191          save_path,
192          :create_bundle,
193          Gitaly::CreateBundleRequest,
194          GitalyClient.long_timeout
195        )
196      end
197
198      def backup_custom_hooks(save_path)
199        gitaly_fetch_stream_to_file(
200          save_path,
201          :backup_custom_hooks,
202          Gitaly::BackupCustomHooksRequest,
203          GitalyClient.default_timeout
204        )
205      end
206
207      def create_from_bundle(bundle_path)
208        gitaly_repo_stream_request(
209          bundle_path,
210          :create_repository_from_bundle,
211          Gitaly::CreateRepositoryFromBundleRequest,
212          GitalyClient.long_timeout
213        )
214      end
215
216      def restore_custom_hooks(custom_hooks_path)
217        gitaly_repo_stream_request(
218          custom_hooks_path,
219          :restore_custom_hooks,
220          Gitaly::RestoreCustomHooksRequest,
221          GitalyClient.default_timeout
222        )
223      end
224
225      def create_from_snapshot(http_url, http_auth)
226        request = Gitaly::CreateRepositoryFromSnapshotRequest.new(
227          repository: @gitaly_repo,
228          http_url: http_url,
229          http_auth: http_auth
230        )
231
232        GitalyClient.call(
233          @storage,
234          :repository_service,
235          :create_repository_from_snapshot,
236          request,
237          timeout: GitalyClient.long_timeout
238        )
239      end
240
241      def write_ref(ref_path, ref, old_ref)
242        request = Gitaly::WriteRefRequest.new(
243          repository: @gitaly_repo,
244          ref: ref_path.b,
245          revision: ref.b
246        )
247        request.old_revision = old_ref.b unless old_ref.nil?
248
249        GitalyClient.call(@storage, :repository_service, :write_ref, request, timeout: GitalyClient.fast_timeout)
250      end
251
252      def set_full_path(path)
253        GitalyClient.call(
254          @storage,
255          :repository_service,
256          :set_full_path,
257          Gitaly::SetFullPathRequest.new(
258            repository: @gitaly_repo,
259            path: path
260          ),
261          timeout: GitalyClient.fast_timeout
262        )
263
264        nil
265      end
266
267      def license_short_name
268        request = Gitaly::FindLicenseRequest.new(repository: @gitaly_repo)
269
270        response = GitalyClient.call(@storage, :repository_service, :find_license, request, timeout: GitalyClient.fast_timeout)
271
272        response.license_short_name.presence
273      end
274
275      def calculate_checksum
276        request  = Gitaly::CalculateChecksumRequest.new(repository: @gitaly_repo)
277        response = GitalyClient.call(@storage, :repository_service, :calculate_checksum, request, timeout: GitalyClient.fast_timeout)
278        response.checksum.presence
279      rescue GRPC::DataLoss => e
280        raise Gitlab::Git::Repository::InvalidRepository, e
281      end
282
283      def raw_changes_between(from, to)
284        request = Gitaly::GetRawChangesRequest.new(repository: @gitaly_repo, from_revision: from, to_revision: to)
285
286        GitalyClient.call(@storage, :repository_service, :get_raw_changes, request, timeout: GitalyClient.fast_timeout)
287      end
288
289      def search_files_by_name(ref, query)
290        request = Gitaly::SearchFilesByNameRequest.new(repository: @gitaly_repo, ref: ref, query: query)
291        GitalyClient.call(@storage, :repository_service, :search_files_by_name, request, timeout: GitalyClient.fast_timeout).flat_map(&:files)
292      end
293
294      def search_files_by_content(ref, query, options = {})
295        request = Gitaly::SearchFilesByContentRequest.new(repository: @gitaly_repo, ref: ref, query: query)
296        response = GitalyClient.call(@storage, :repository_service, :search_files_by_content, request, timeout: GitalyClient.default_timeout)
297        search_results_from_response(response, options)
298      end
299
300      def search_files_by_regexp(ref, filter)
301        request = Gitaly::SearchFilesByNameRequest.new(repository: @gitaly_repo, ref: ref, query: '.', filter: filter)
302        GitalyClient.call(@storage, :repository_service, :search_files_by_name, request, timeout: GitalyClient.fast_timeout).flat_map(&:files)
303      end
304
305      def disconnect_alternates
306        request = Gitaly::DisconnectGitAlternatesRequest.new(
307          repository: @gitaly_repo
308        )
309
310        GitalyClient.call(@storage, :object_pool_service, :disconnect_git_alternates, request, timeout: GitalyClient.long_timeout)
311      end
312
313      def rename(relative_path)
314        request = Gitaly::RenameRepositoryRequest.new(repository: @gitaly_repo, relative_path: relative_path)
315
316        GitalyClient.call(@storage, :repository_service, :rename_repository, request, timeout: GitalyClient.fast_timeout)
317      end
318
319      def remove
320        request = Gitaly::RemoveRepositoryRequest.new(repository: @gitaly_repo)
321
322        GitalyClient.call(@storage, :repository_service, :remove_repository, request, timeout: GitalyClient.long_timeout)
323      end
324
325      def replicate(source_repository)
326        request = Gitaly::ReplicateRepositoryRequest.new(
327          repository: @gitaly_repo,
328          source: source_repository.gitaly_repository
329        )
330
331        GitalyClient.call(
332          @storage,
333          :repository_service,
334          :replicate_repository,
335          request,
336          remote_storage: source_repository.storage,
337          timeout: GitalyClient.long_timeout
338        )
339      end
340
341      private
342
343      def search_results_from_response(gitaly_response, options = {})
344        limit = options[:limit]
345
346        matches = []
347        matches_count = 0
348        current_match = +""
349
350        gitaly_response.each do |message|
351          next if message.nil?
352
353          break if limit && matches_count >= limit
354
355          current_match << message.match_data
356
357          if message.end_of_match
358            matches << current_match
359            current_match = +""
360            matches_count += 1
361          end
362        end
363
364        matches
365      end
366
367      def gitaly_fetch_stream_to_file(save_path, rpc_name, request_class, timeout)
368        request = request_class.new(repository: @gitaly_repo)
369        response = GitalyClient.call(
370          @storage,
371          :repository_service,
372          rpc_name,
373          request,
374          timeout: timeout
375        )
376        write_stream_to_file(response, save_path)
377      end
378
379      def write_stream_to_file(response, save_path)
380        File.open(save_path, 'wb') do |f|
381          response.each do |message|
382            f.write(message.data)
383          end
384        end
385        # If the file is empty means that we received an empty stream, we delete the file
386        FileUtils.rm(save_path) if File.zero?(save_path)
387      end
388
389      def gitaly_repo_stream_request(file_path, rpc_name, request_class, timeout)
390        request = request_class.new(repository: @gitaly_repo)
391        enum = Enumerator.new do |y|
392          File.open(file_path, 'rb') do |f|
393            while data = f.read(MAX_MSG_SIZE)
394              request.data = data
395
396              y.yield request
397              request = request_class.new
398            end
399          end
400        end
401
402        GitalyClient.call(
403          @storage,
404          :repository_service,
405          rpc_name,
406          enum,
407          timeout: timeout
408        )
409      end
410
411      def build_set_config_entry(key, value)
412        entry = Gitaly::SetConfigRequest::Entry.new(key: key)
413
414        case value
415        when String
416          entry.value_str = value
417        when Integer
418          entry.value_int32 = value
419        when TrueClass, FalseClass
420          entry.value_bool = value
421        else
422          raise InvalidArgument, "invalid git config value: #{value.inspect}"
423        end
424
425        entry
426      end
427    end
428  end
429end
430