1# frozen_string_literal: true 2 3module Gitlab 4 module GitalyClient 5 class CommitService 6 include Gitlab::EncodingHelper 7 8 def initialize(repository) 9 @gitaly_repo = repository.gitaly_repository 10 @repository = repository 11 end 12 13 def ls_files(revision) 14 request = Gitaly::ListFilesRequest.new( 15 repository: @gitaly_repo, 16 revision: encode_binary(revision) 17 ) 18 19 response = GitalyClient.call(@repository.storage, :commit_service, :list_files, request, timeout: GitalyClient.medium_timeout) 20 response.flat_map do |msg| 21 msg.paths.map { |d| EncodingHelper.encode!(d.dup) } 22 end 23 end 24 25 def ancestor?(ancestor_id, child_id) 26 request = Gitaly::CommitIsAncestorRequest.new( 27 repository: @gitaly_repo, 28 ancestor_id: ancestor_id, 29 child_id: child_id 30 ) 31 32 GitalyClient.call(@repository.storage, :commit_service, :commit_is_ancestor, request, timeout: GitalyClient.fast_timeout).value 33 end 34 35 def diff(from, to, options = {}) 36 from_id = case from 37 when NilClass 38 Gitlab::Git::EMPTY_TREE_ID 39 else 40 if from.respond_to?(:oid) 41 # This is meant to match a Rugged::Commit. This should be impossible in 42 # the future. 43 from.oid 44 else 45 from 46 end 47 end 48 49 to_id = case to 50 when NilClass 51 Gitlab::Git::EMPTY_TREE_ID 52 else 53 if to.respond_to?(:oid) 54 # This is meant to match a Rugged::Commit. This should be impossible in 55 # the future. 56 to.oid 57 else 58 to 59 end 60 end 61 62 request_params = diff_between_commits_request_params(from_id, to_id, options) 63 64 call_commit_diff(request_params, options) 65 end 66 67 def diff_from_parent(commit, options = {}) 68 request_params = diff_from_parent_request_params(commit, options) 69 70 call_commit_diff(request_params, options) 71 end 72 73 def commit_deltas(commit) 74 request = Gitaly::CommitDeltaRequest.new(diff_from_parent_request_params(commit)) 75 response = GitalyClient.call(@repository.storage, :diff_service, :commit_delta, request, timeout: GitalyClient.fast_timeout) 76 response.flat_map { |msg| msg.deltas } 77 end 78 79 def tree_entry(ref, path, limit = nil) 80 if Pathname.new(path).cleanpath.to_s.start_with?('../') 81 # The TreeEntry RPC should return an empty response in this case but in 82 # Gitaly 0.107.0 and earlier we get an exception instead. This early return 83 # saves us a Gitaly roundtrip while also avoiding the exception. 84 return 85 end 86 87 request = Gitaly::TreeEntryRequest.new( 88 repository: @gitaly_repo, 89 revision: encode_binary(ref), 90 path: encode_binary(path), 91 limit: limit.to_i 92 ) 93 94 response = GitalyClient.call(@repository.storage, :commit_service, :tree_entry, request, timeout: GitalyClient.medium_timeout) 95 96 entry = nil 97 data = [] 98 response.each do |msg| 99 if entry.nil? 100 entry = msg 101 102 break unless entry.type == :BLOB 103 end 104 105 data << msg.data 106 end 107 entry.data = data.join 108 109 entry unless entry.oid.blank? 110 rescue GRPC::NotFound 111 nil 112 end 113 114 def tree_entries(repository, revision, path, recursive, pagination_params) 115 request = Gitaly::GetTreeEntriesRequest.new( 116 repository: @gitaly_repo, 117 revision: encode_binary(revision), 118 path: path.present? ? encode_binary(path) : '.', 119 recursive: recursive, 120 pagination_params: pagination_params 121 ) 122 request.sort = Gitaly::GetTreeEntriesRequest::SortBy::TREES_FIRST if pagination_params 123 124 response = GitalyClient.call(@repository.storage, :commit_service, :get_tree_entries, request, timeout: GitalyClient.medium_timeout) 125 126 cursor = nil 127 128 entries = response.flat_map do |message| 129 cursor = message.pagination_cursor if message.pagination_cursor 130 131 message.entries.map do |gitaly_tree_entry| 132 Gitlab::Git::Tree.new( 133 id: gitaly_tree_entry.oid, 134 root_id: gitaly_tree_entry.root_oid, 135 type: gitaly_tree_entry.type.downcase, 136 mode: gitaly_tree_entry.mode.to_s(8), 137 name: File.basename(gitaly_tree_entry.path), 138 path: encode_binary(gitaly_tree_entry.path), 139 flat_path: encode_binary(gitaly_tree_entry.flat_path), 140 commit_id: gitaly_tree_entry.commit_oid 141 ) 142 end 143 end 144 145 [entries, cursor] 146 end 147 148 def commit_count(ref, options = {}) 149 request = Gitaly::CountCommitsRequest.new( 150 repository: @gitaly_repo, 151 revision: encode_binary(ref), 152 all: !!options[:all], 153 first_parent: !!options[:first_parent] 154 ) 155 request.after = Google::Protobuf::Timestamp.new(seconds: options[:after].to_i) if options[:after].present? 156 request.before = Google::Protobuf::Timestamp.new(seconds: options[:before].to_i) if options[:before].present? 157 request.path = encode_binary(options[:path]) if options[:path].present? 158 request.max_count = options[:max_count] if options[:max_count].present? 159 160 GitalyClient.call(@repository.storage, :commit_service, :count_commits, request, timeout: GitalyClient.medium_timeout).count 161 end 162 163 def diverging_commit_count(from, to, max_count:) 164 request = Gitaly::CountDivergingCommitsRequest.new( 165 repository: @gitaly_repo, 166 from: encode_binary(from), 167 to: encode_binary(to), 168 max_count: max_count 169 ) 170 response = GitalyClient.call(@repository.storage, :commit_service, :count_diverging_commits, request, timeout: GitalyClient.medium_timeout) 171 [response.left_count, response.right_count] 172 end 173 174 def list_last_commits_for_tree(revision, path, offset: 0, limit: 25, literal_pathspec: false) 175 request = Gitaly::ListLastCommitsForTreeRequest.new( 176 repository: @gitaly_repo, 177 revision: encode_binary(revision), 178 path: encode_binary(path.to_s), 179 offset: offset, 180 limit: limit, 181 global_options: parse_global_options!(literal_pathspec: literal_pathspec) 182 ) 183 184 response = GitalyClient.call(@repository.storage, :commit_service, :list_last_commits_for_tree, request, timeout: GitalyClient.medium_timeout) 185 186 response.each_with_object({}) do |gitaly_response, hsh| 187 gitaly_response.commits.each do |commit_for_tree| 188 hsh[commit_for_tree.path_bytes] = Gitlab::Git::Commit.new(@repository, commit_for_tree.commit) 189 end 190 end 191 end 192 193 def last_commit_for_path(revision, path, literal_pathspec: false) 194 request = Gitaly::LastCommitForPathRequest.new( 195 repository: @gitaly_repo, 196 revision: encode_binary(revision), 197 path: encode_binary(path.to_s), 198 global_options: parse_global_options!(literal_pathspec: literal_pathspec) 199 ) 200 201 gitaly_commit = GitalyClient.call(@repository.storage, :commit_service, :last_commit_for_path, request, timeout: GitalyClient.fast_timeout).commit 202 return unless gitaly_commit 203 204 Gitlab::Git::Commit.new(@repository, gitaly_commit) 205 end 206 207 def diff_stats(left_commit_sha, right_commit_sha) 208 request = Gitaly::DiffStatsRequest.new( 209 repository: @gitaly_repo, 210 left_commit_id: left_commit_sha, 211 right_commit_id: right_commit_sha 212 ) 213 214 response = GitalyClient.call(@repository.storage, :diff_service, :diff_stats, request, timeout: GitalyClient.medium_timeout) 215 response.flat_map(&:stats) 216 end 217 218 def find_changed_paths(commits) 219 request = Gitaly::FindChangedPathsRequest.new( 220 repository: @gitaly_repo, 221 commits: commits 222 ) 223 224 response = GitalyClient.call(@repository.storage, :diff_service, :find_changed_paths, request, timeout: GitalyClient.medium_timeout) 225 response.flat_map do |msg| 226 msg.paths.map do |path| 227 Gitlab::Git::ChangedPath.new( 228 status: path.status, 229 path: EncodingHelper.encode!(path.path) 230 ) 231 end 232 end 233 end 234 235 def find_all_commits(opts = {}) 236 request = Gitaly::FindAllCommitsRequest.new( 237 repository: @gitaly_repo, 238 revision: opts[:ref].to_s, 239 max_count: opts[:max_count].to_i, 240 skip: opts[:skip].to_i 241 ) 242 request.order = opts[:order].upcase if opts[:order].present? 243 244 response = GitalyClient.call(@repository.storage, :commit_service, :find_all_commits, request, timeout: GitalyClient.medium_timeout) 245 consume_commits_response(response) 246 end 247 248 def list_commits(revisions, reverse: false, pagination_params: nil) 249 request = Gitaly::ListCommitsRequest.new( 250 repository: @gitaly_repo, 251 revisions: Array.wrap(revisions), 252 reverse: reverse, 253 pagination_params: pagination_params 254 ) 255 256 response = GitalyClient.call(@repository.storage, :commit_service, :list_commits, request, timeout: GitalyClient.medium_timeout) 257 consume_commits_response(response) 258 end 259 260 # List all commits which are new in the repository. If commits have been pushed into the repo 261 def list_new_commits(revisions, allow_quarantine: false) 262 git_env = Gitlab::Git::HookEnv.all(@gitaly_repo.gl_repository) 263 if allow_quarantine && git_env['GIT_OBJECT_DIRECTORY_RELATIVE'].present? 264 # If we have a quarantine environment, then we can optimize the check 265 # by doing a ListAllCommitsRequest. Instead of walking through 266 # references, we just walk through all quarantined objects, which is 267 # a lot more efficient. To do so, we throw away any alternate object 268 # directories, which point to the main object directory of the 269 # repository, and only keep the object directory which points into 270 # the quarantine object directory. 271 quarantined_repo = @gitaly_repo.dup 272 quarantined_repo.git_alternate_object_directories = Google::Protobuf::RepeatedField.new(:string) 273 274 request = Gitaly::ListAllCommitsRequest.new( 275 repository: quarantined_repo 276 ) 277 278 response = GitalyClient.call(@repository.storage, :commit_service, :list_all_commits, request, timeout: GitalyClient.medium_timeout) 279 consume_commits_response(response) 280 else 281 list_commits(Array.wrap(revisions) + %w[--not --all]) 282 end 283 end 284 285 def list_commits_by_oid(oids) 286 return [] if oids.empty? 287 288 request = Gitaly::ListCommitsByOidRequest.new(repository: @gitaly_repo, oid: oids) 289 290 response = GitalyClient.call(@repository.storage, :commit_service, :list_commits_by_oid, request, timeout: GitalyClient.medium_timeout) 291 consume_commits_response(response) 292 rescue GRPC::NotFound # If no repository is found, happens mainly during testing 293 [] 294 end 295 296 def commits_by_message(query, revision: '', path: '', limit: 1000, offset: 0, literal_pathspec: true) 297 request = Gitaly::CommitsByMessageRequest.new( 298 repository: @gitaly_repo, 299 query: query, 300 revision: encode_binary(revision), 301 path: encode_binary(path), 302 limit: limit.to_i, 303 offset: offset.to_i, 304 global_options: parse_global_options!(literal_pathspec: literal_pathspec) 305 ) 306 307 response = GitalyClient.call(@repository.storage, :commit_service, :commits_by_message, request, timeout: GitalyClient.medium_timeout) 308 consume_commits_response(response) 309 end 310 311 def languages(ref = nil) 312 request = Gitaly::CommitLanguagesRequest.new(repository: @gitaly_repo, revision: ref || '') 313 response = GitalyClient.call(@repository.storage, :commit_service, :commit_languages, request, timeout: GitalyClient.long_timeout) 314 315 response.languages.map { |l| { value: l.share.round(2), label: l.name, color: l.color, highlight: l.color } } 316 end 317 318 def raw_blame(revision, path) 319 request = Gitaly::RawBlameRequest.new( 320 repository: @gitaly_repo, 321 revision: encode_binary(revision), 322 path: encode_binary(path) 323 ) 324 325 response = GitalyClient.call(@repository.storage, :commit_service, :raw_blame, request, timeout: GitalyClient.medium_timeout) 326 response.reduce([]) { |memo, msg| memo << msg.data }.join 327 end 328 329 def find_commit(revision) 330 return call_find_commit(revision) unless Gitlab::SafeRequestStore.active? 331 332 # We don't use Gitlab::SafeRequestStore.fetch(key) { ... } directly 333 # because `revision` can be a branch name, so we can't use it as a key 334 # as it could point to another commit later on (happens a lot in 335 # tests). 336 key = { 337 storage: @gitaly_repo.storage_name, 338 relative_path: @gitaly_repo.relative_path, 339 commit_id: revision 340 } 341 return Gitlab::SafeRequestStore[key] if Gitlab::SafeRequestStore.exist?(key) 342 343 commit = call_find_commit(revision) 344 345 if GitalyClient.ref_name_caching_allowed? 346 Gitlab::SafeRequestStore[key] = commit 347 return commit 348 end 349 350 return unless commit 351 352 key[:commit_id] = commit.id 353 Gitlab::SafeRequestStore[key] = commit 354 end 355 356 def commit_stats(revision) 357 request = Gitaly::CommitStatsRequest.new( 358 repository: @gitaly_repo, 359 revision: encode_binary(revision) 360 ) 361 GitalyClient.call(@repository.storage, :commit_service, :commit_stats, request, timeout: GitalyClient.medium_timeout) 362 end 363 364 def find_commits(options) 365 request = Gitaly::FindCommitsRequest.new( 366 repository: @gitaly_repo, 367 limit: options[:limit], 368 offset: options[:offset], 369 follow: options[:follow], 370 skip_merges: options[:skip_merges], 371 all: !!options[:all], 372 first_parent: !!options[:first_parent], 373 global_options: parse_global_options!(options), 374 disable_walk: true, # This option is deprecated. The 'walk' implementation is being removed. 375 trailers: options[:trailers] 376 ) 377 request.after = GitalyClient.timestamp(options[:after]) if options[:after] 378 request.before = GitalyClient.timestamp(options[:before]) if options[:before] 379 request.revision = encode_binary(options[:ref]) if options[:ref] 380 request.author = encode_binary(options[:author]) if options[:author] 381 request.order = options[:order].upcase.sub('DEFAULT', 'NONE') if options[:order].present? 382 383 request.paths = encode_repeated(Array(options[:path])) if options[:path].present? 384 385 response = GitalyClient.call(@repository.storage, :commit_service, :find_commits, request, timeout: GitalyClient.medium_timeout) 386 consume_commits_response(response) 387 end 388 389 def filter_shas_with_signatures(shas) 390 request = Gitaly::FilterShasWithSignaturesRequest.new(repository: @gitaly_repo) 391 392 enum = Enumerator.new do |y| 393 shas.each_slice(20) do |revs| 394 request.shas = encode_repeated(revs) 395 396 y.yield request 397 398 request = Gitaly::FilterShasWithSignaturesRequest.new 399 end 400 end 401 402 response = GitalyClient.call(@repository.storage, :commit_service, :filter_shas_with_signatures, enum, timeout: GitalyClient.fast_timeout) 403 response.flat_map do |msg| 404 msg.shas.map { |sha| EncodingHelper.encode!(sha) } 405 end 406 end 407 408 def get_commit_signatures(commit_ids) 409 request = Gitaly::GetCommitSignaturesRequest.new(repository: @gitaly_repo, commit_ids: commit_ids) 410 response = GitalyClient.call(@repository.storage, :commit_service, :get_commit_signatures, request, timeout: GitalyClient.fast_timeout) 411 412 signatures = Hash.new { |h, k| h[k] = [+''.b, +''.b] } 413 current_commit_id = nil 414 415 response.each do |message| 416 current_commit_id = message.commit_id if message.commit_id.present? 417 418 signatures[current_commit_id].first << message.signature 419 signatures[current_commit_id].last << message.signed_text 420 end 421 422 signatures 423 rescue GRPC::InvalidArgument => ex 424 raise ArgumentError, ex 425 end 426 427 def get_commit_messages(commit_ids) 428 request = Gitaly::GetCommitMessagesRequest.new(repository: @gitaly_repo, commit_ids: commit_ids) 429 response = GitalyClient.call(@repository.storage, :commit_service, :get_commit_messages, request, timeout: GitalyClient.fast_timeout) 430 431 messages = Hash.new { |h, k| h[k] = +''.b } 432 current_commit_id = nil 433 434 response.each do |rpc_message| 435 current_commit_id = rpc_message.commit_id if rpc_message.commit_id.present? 436 437 messages[current_commit_id] << rpc_message.message 438 end 439 440 messages 441 end 442 443 def list_commits_by_ref_name(refs) 444 request = Gitaly::ListCommitsByRefNameRequest 445 .new(repository: @gitaly_repo, ref_names: refs.map { |ref| encode_binary(ref) }) 446 447 response = GitalyClient.call(@repository.storage, :commit_service, :list_commits_by_ref_name, request, timeout: GitalyClient.medium_timeout) 448 449 commit_refs = response.flat_map do |message| 450 message.commit_refs.map do |commit_ref| 451 [encode_utf8(commit_ref.ref_name), Gitlab::Git::Commit.new(@repository, commit_ref.commit)] 452 end 453 end 454 455 Hash[commit_refs] 456 end 457 458 private 459 460 def parse_global_options!(options) 461 literal_pathspec = options.delete(:literal_pathspec) 462 Gitaly::GlobalOptions.new(literal_pathspecs: literal_pathspec) 463 end 464 465 def call_commit_diff(request_params, options = {}) 466 request_params[:ignore_whitespace_change] = options.fetch(:ignore_whitespace_change, false) 467 request_params[:enforce_limits] = options.fetch(:limits, true) 468 request_params[:collapse_diffs] = !options.fetch(:expanded, true) 469 request_params.merge!(Gitlab::Git::DiffCollection.limits(options).to_h) 470 471 request = Gitaly::CommitDiffRequest.new(request_params) 472 response = GitalyClient.call(@repository.storage, :diff_service, :commit_diff, request, timeout: GitalyClient.medium_timeout) 473 GitalyClient::DiffStitcher.new(response) 474 end 475 476 def diff_from_parent_request_params(commit, options = {}) 477 parent_id = commit.parent_ids.first || Gitlab::Git::EMPTY_TREE_ID 478 479 diff_between_commits_request_params(parent_id, commit.id, options) 480 end 481 482 def diff_between_commits_request_params(from_id, to_id, options) 483 { 484 repository: @gitaly_repo, 485 left_commit_id: from_id, 486 right_commit_id: to_id, 487 paths: options.fetch(:paths, []).compact.map { |path| encode_binary(path) } 488 } 489 end 490 491 def consume_commits_response(response) 492 response.flat_map do |message| 493 message.commits.map do |gitaly_commit| 494 Gitlab::Git::Commit.new(@repository, gitaly_commit) 495 end 496 end 497 end 498 499 def encode_repeated(array) 500 Google::Protobuf::RepeatedField.new(:bytes, array.map { |s| encode_binary(s) } ) 501 end 502 503 def call_find_commit(revision) 504 request = Gitaly::FindCommitRequest.new( 505 repository: @gitaly_repo, 506 revision: encode_binary(revision) 507 ) 508 509 response = GitalyClient.call(@repository.storage, :commit_service, :find_commit, request, timeout: GitalyClient.medium_timeout) 510 511 response.commit 512 end 513 end 514 end 515end 516