1# frozen_string_literal: true 2 3require "spec_helper" 4 5RSpec.describe Gitlab::Git::Repository, :seed_helper do 6 include Gitlab::EncodingHelper 7 include RepoHelpers 8 using RSpec::Parameterized::TableSyntax 9 10 shared_examples 'wrapping gRPC errors' do |gitaly_client_class, gitaly_client_method| 11 it 'wraps gRPC not found error' do 12 expect_any_instance_of(gitaly_client_class).to receive(gitaly_client_method) 13 .and_raise(GRPC::NotFound) 14 expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository) 15 end 16 17 it 'wraps gRPC unknown error' do 18 expect_any_instance_of(gitaly_client_class).to receive(gitaly_client_method) 19 .and_raise(GRPC::Unknown) 20 expect { subject }.to raise_error(Gitlab::Git::CommandError) 21 end 22 end 23 24 let(:mutable_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '', 'group/project') } 25 let(:mutable_repository_path) { File.join(TestEnv.repos_path, mutable_repository.relative_path) } 26 let(:mutable_repository_rugged) { Rugged::Repository.new(mutable_repository_path) } 27 let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } 28 let(:repository_path) { File.join(TestEnv.repos_path, repository.relative_path) } 29 let(:repository_rugged) { Rugged::Repository.new(repository_path) } 30 let(:storage_path) { TestEnv.repos_path } 31 let(:user) { build(:user) } 32 33 describe "Respond to" do 34 subject { repository } 35 36 it { is_expected.to respond_to(:root_ref) } 37 it { is_expected.to respond_to(:tags) } 38 end 39 40 describe '#root_ref' do 41 it 'returns UTF-8' do 42 expect(repository.root_ref).to be_utf8 43 end 44 45 it 'gets the branch name from GitalyClient' do 46 expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:default_branch_name) 47 repository.root_ref 48 end 49 50 it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :default_branch_name do 51 subject { repository.root_ref } 52 end 53 end 54 55 describe '#create_repository' do 56 it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RepositoryService, :create_repository do 57 subject { repository.create_repository } 58 end 59 end 60 61 describe '#branch_names' do 62 subject { repository.branch_names } 63 64 it 'has SeedRepo::Repo::BRANCHES.size elements' do 65 expect(subject.size).to eq(SeedRepo::Repo::BRANCHES.size) 66 end 67 68 it 'returns UTF-8' do 69 expect(subject.first).to be_utf8 70 end 71 72 it { is_expected.to include("master") } 73 it { is_expected.not_to include("branch-from-space") } 74 75 it 'gets the branch names from GitalyClient' do 76 expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:branch_names) 77 subject 78 end 79 80 it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :branch_names 81 end 82 83 describe '#tag_names' do 84 subject { repository.tag_names } 85 86 it { is_expected.to be_kind_of Array } 87 88 it 'has SeedRepo::Repo::TAGS.size elements' do 89 expect(subject.size).to eq(SeedRepo::Repo::TAGS.size) 90 end 91 92 it 'returns UTF-8' do 93 expect(subject.first).to be_utf8 94 end 95 96 describe '#last' do 97 subject { super().last } 98 99 it { is_expected.to eq("v1.2.1") } 100 end 101 it { is_expected.to include("v1.0.0") } 102 it { is_expected.not_to include("v5.0.0") } 103 104 it 'gets the tag names from GitalyClient' do 105 expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:tag_names) 106 subject 107 end 108 109 it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :tag_names 110 end 111 112 describe '#tags' do 113 subject { repository.tags } 114 115 it 'gets tags from GitalyClient' do 116 expect_next_instance_of(Gitlab::GitalyClient::RefService) do |service| 117 expect(service).to receive(:tags) 118 end 119 120 subject 121 end 122 123 context 'with sorting option' do 124 subject { repository.tags(sort_by: 'name_asc') } 125 126 it 'gets tags from GitalyClient' do 127 expect_next_instance_of(Gitlab::GitalyClient::RefService) do |service| 128 expect(service).to receive(:tags).with(sort_by: 'name_asc', pagination_params: nil) 129 end 130 131 subject 132 end 133 end 134 135 context 'with pagination option' do 136 subject { repository.tags(pagination_params: { limit: 5, page_token: 'refs/tags/v1.0.0' }) } 137 138 it 'gets tags from GitalyClient' do 139 expect_next_instance_of(Gitlab::GitalyClient::RefService) do |service| 140 expect(service).to receive(:tags).with( 141 sort_by: nil, 142 pagination_params: { limit: 5, page_token: 'refs/tags/v1.0.0' } 143 ) 144 end 145 146 subject 147 end 148 end 149 150 it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :tags 151 end 152 153 describe '#archive_metadata' do 154 let(:storage_path) { '/tmp' } 155 let(:cache_key) { File.join(repository.gl_repository, SeedRepo::LastCommit::ID) } 156 157 let(:append_sha) { true } 158 let(:ref) { 'master' } 159 let(:format) { nil } 160 let(:path) { nil } 161 162 let(:expected_extension) { 'tar.gz' } 163 let(:expected_filename) { "#{expected_prefix}.#{expected_extension}" } 164 let(:expected_path) { File.join(storage_path, cache_key, "@v2", expected_filename) } 165 let(:expected_prefix) { "gitlab-git-test-#{ref}-#{SeedRepo::LastCommit::ID}" } 166 167 subject(:metadata) { repository.archive_metadata(ref, storage_path, 'gitlab-git-test', format, append_sha: append_sha, path: path) } 168 169 it 'sets CommitId to the commit SHA' do 170 expect(metadata['CommitId']).to eq(SeedRepo::LastCommit::ID) 171 end 172 173 it 'sets ArchivePrefix to the expected prefix' do 174 expect(metadata['ArchivePrefix']).to eq(expected_prefix) 175 end 176 177 it 'sets ArchivePath to the expected globally-unique path' do 178 expect(expected_path).to include(File.join(repository.gl_repository, SeedRepo::LastCommit::ID)) 179 180 expect(metadata['ArchivePath']).to eq(expected_path) 181 end 182 183 context 'path is set' do 184 let(:path) { 'foo/bar' } 185 186 it 'appends the path to the prefix' do 187 expect(metadata['ArchivePrefix']).to eq("#{expected_prefix}-foo-bar") 188 end 189 end 190 191 context 'append_sha varies archive path and filename' do 192 where(:append_sha, :ref, :expected_prefix) do 193 sha = SeedRepo::LastCommit::ID 194 195 true | 'master' | "gitlab-git-test-master-#{sha}" 196 true | sha | "gitlab-git-test-#{sha}-#{sha}" 197 false | 'master' | "gitlab-git-test-master" 198 false | sha | "gitlab-git-test-#{sha}" 199 nil | 'master' | "gitlab-git-test-master-#{sha}" 200 nil | sha | "gitlab-git-test-#{sha}" 201 end 202 203 with_them do 204 it { expect(metadata['ArchivePrefix']).to eq(expected_prefix) } 205 it { expect(metadata['ArchivePath']).to eq(expected_path) } 206 end 207 end 208 209 context 'format varies archive path and filename' do 210 where(:format, :expected_extension) do 211 nil | 'tar.gz' 212 'madeup' | 'tar.gz' 213 'tbz2' | 'tar.bz2' 214 'zip' | 'zip' 215 end 216 217 with_them do 218 it { expect(metadata['ArchivePrefix']).to eq(expected_prefix) } 219 it { expect(metadata['ArchivePath']).to eq(expected_path) } 220 end 221 end 222 end 223 224 describe '#size' do 225 subject { repository.size } 226 227 it { is_expected.to be < 2 } 228 end 229 230 describe '#to_s' do 231 subject { repository.to_s } 232 233 it { is_expected.to eq("<Gitlab::Git::Repository: group/project>") } 234 end 235 236 describe '#object_directory_size' do 237 before do 238 allow(repository.gitaly_repository_client) 239 .to receive(:get_object_directory_size) 240 .and_return(2) 241 end 242 243 subject { repository.object_directory_size } 244 245 it { is_expected.to eq 2048 } 246 end 247 248 describe '#empty?' do 249 it { expect(repository).not_to be_empty } 250 end 251 252 describe '#ref_names' do 253 let(:ref_names) { repository.ref_names } 254 255 subject { ref_names } 256 257 it { is_expected.to be_kind_of Array } 258 259 describe '#first' do 260 subject { super().first } 261 262 it { is_expected.to eq('feature') } 263 end 264 265 describe '#last' do 266 subject { super().last } 267 268 it { is_expected.to eq('v1.2.1') } 269 end 270 end 271 272 describe '#submodule_url_for' do 273 let(:ref) { 'master' } 274 275 def submodule_url(path) 276 repository.submodule_url_for(ref, path) 277 end 278 279 it { expect(submodule_url('six')).to eq('git://github.com/randx/six.git') } 280 it { expect(submodule_url('nested/six')).to eq('git://github.com/randx/six.git') } 281 it { expect(submodule_url('deeper/nested/six')).to eq('git://github.com/randx/six.git') } 282 it { expect(submodule_url('invalid/path')).to eq(nil) } 283 284 context 'uncommitted submodule dir' do 285 let(:ref) { 'fix-existing-submodule-dir' } 286 287 it { expect(submodule_url('submodule-existing-dir')).to eq(nil) } 288 end 289 290 context 'tags' do 291 let(:ref) { 'v1.2.1' } 292 293 it { expect(submodule_url('six')).to eq('git://github.com/randx/six.git') } 294 end 295 296 context 'no .gitmodules at commit' do 297 let(:ref) { '9596bc54a6f0c0c98248fe97077eb5ccf48a98d0' } 298 299 it { expect(submodule_url('six')).to eq(nil) } 300 end 301 302 context 'no gitlink entry' do 303 let(:ref) { '6d39438' } 304 305 it { expect(submodule_url('six')).to eq(nil) } 306 end 307 end 308 309 describe '#submodule_urls_for' do 310 let(:ref) { 'master' } 311 312 it 'returns url mappings for submodules' do 313 urls = repository.submodule_urls_for(ref) 314 315 expect(urls).to eq({ 316 "deeper/nested/six" => "git://github.com/randx/six.git", 317 "gitlab-grack" => "https://gitlab.com/gitlab-org/gitlab-grack.git", 318 "gitlab-shell" => "https://github.com/gitlabhq/gitlab-shell.git", 319 "nested/six" => "git://github.com/randx/six.git", 320 "six" => "git://github.com/randx/six.git" 321 }) 322 end 323 end 324 325 describe '#commit_count' do 326 it { expect(repository.commit_count("master")).to eq(25) } 327 it { expect(repository.commit_count("feature")).to eq(9) } 328 it { expect(repository.commit_count("does-not-exist")).to eq(0) } 329 330 it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::CommitService, :commit_count do 331 subject { repository.commit_count('master') } 332 end 333 end 334 335 describe '#diverging_commit_count' do 336 it 'counts 0 for the same branch' do 337 expect(repository.diverging_commit_count('master', 'master', max_count: 1000)).to eq([0, 0]) 338 end 339 340 context 'max count does not truncate results' do 341 where(:left, :right, :expected) do 342 1 | 1 | [1, 1] 343 4 | 4 | [4, 4] 344 2 | 2 | [2, 2] 345 2 | 4 | [2, 4] 346 4 | 2 | [4, 2] 347 10 | 10 | [10, 10] 348 end 349 350 with_them do 351 before do 352 repository.create_branch('left-branch') 353 repository.create_branch('right-branch') 354 355 left.times do 356 new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'left-branch', 'some more content for a', 'some stuff') 357 end 358 359 right.times do 360 new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'right-branch', 'some more content for b', 'some stuff') 361 end 362 end 363 364 after do 365 repository.delete_branch('left-branch') 366 repository.delete_branch('right-branch') 367 end 368 369 it 'returns the correct count bounding at max_count' do 370 branch_a_sha = repository_rugged.branches['left-branch'].target.oid 371 branch_b_sha = repository_rugged.branches['right-branch'].target.oid 372 373 count = repository.diverging_commit_count(branch_a_sha, branch_b_sha, max_count: 1000) 374 375 expect(count).to eq(expected) 376 end 377 end 378 end 379 380 context 'max count truncates results' do 381 where(:left, :right, :max_count) do 382 1 | 1 | 1 383 4 | 4 | 4 384 2 | 2 | 3 385 2 | 4 | 3 386 4 | 2 | 5 387 10 | 10 | 10 388 end 389 390 with_them do 391 before do 392 repository.create_branch('left-branch') 393 repository.create_branch('right-branch') 394 395 left.times do 396 new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'left-branch', 'some more content for a', 'some stuff') 397 end 398 399 right.times do 400 new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'right-branch', 'some more content for b', 'some stuff') 401 end 402 end 403 404 after do 405 repository.delete_branch('left-branch') 406 repository.delete_branch('right-branch') 407 end 408 409 it 'returns the correct count bounding at max_count' do 410 branch_a_sha = repository_rugged.branches['left-branch'].target.oid 411 branch_b_sha = repository_rugged.branches['right-branch'].target.oid 412 413 results = repository.diverging_commit_count(branch_a_sha, branch_b_sha, max_count: max_count) 414 415 expect(results[0] + results[1]).to eq(max_count) 416 end 417 end 418 end 419 420 it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::CommitService, :diverging_commit_count do 421 subject { repository.diverging_commit_count('master', 'master', max_count: 1000) } 422 end 423 end 424 425 describe '#has_local_branches?' do 426 context 'check for local branches' do 427 it { expect(repository.has_local_branches?).to eq(true) } 428 429 context 'mutable' do 430 let(:repository) { mutable_repository } 431 432 after do 433 ensure_seeds 434 end 435 436 it 'returns false when there are no branches' do 437 # Sanity check 438 expect(repository.has_local_branches?).to eq(true) 439 440 FileUtils.rm_rf(File.join(repository_path, 'packed-refs')) 441 heads_dir = File.join(repository_path, 'refs/heads') 442 FileUtils.rm_rf(heads_dir) 443 FileUtils.mkdir_p(heads_dir) 444 445 repository.expire_has_local_branches_cache 446 expect(repository.has_local_branches?).to eq(false) 447 end 448 end 449 450 context 'memoizes the value' do 451 it 'returns true' do 452 expect(repository).to receive(:uncached_has_local_branches?).once.and_call_original 453 454 2.times do 455 expect(repository.has_local_branches?).to eq(true) 456 end 457 end 458 end 459 end 460 end 461 462 describe '#delete_refs' do 463 let(:repository) { mutable_repository } 464 465 after do 466 ensure_seeds 467 end 468 469 it 'deletes the ref' do 470 repository.delete_refs('refs/heads/feature') 471 472 expect(repository_rugged.references['refs/heads/feature']).to be_nil 473 end 474 475 it 'deletes all refs' do 476 refs = %w[refs/heads/wip refs/tags/v1.1.0] 477 repository.delete_refs(*refs) 478 479 refs.each do |ref| 480 expect(repository_rugged.references[ref]).to be_nil 481 end 482 end 483 484 it 'does not fail when deleting an empty list of refs' do 485 expect { repository.delete_refs(*[]) }.not_to raise_error 486 end 487 488 it 'raises an error if it failed' do 489 expect { repository.delete_refs('refs\heads\fix') }.to raise_error(Gitlab::Git::Repository::GitError) 490 end 491 end 492 493 describe '#branch_names_contains_sha' do 494 let(:head_id) { repository_rugged.head.target.oid } 495 let(:new_branch) { head_id } 496 let(:utf8_branch) { 'branch-é' } 497 498 before do 499 repository.create_branch(new_branch) 500 repository.create_branch(utf8_branch) 501 end 502 503 after do 504 repository.delete_branch(new_branch) 505 repository.delete_branch(utf8_branch) 506 end 507 508 it 'displays that branch' do 509 expect(repository.branch_names_contains_sha(head_id)).to include('master', new_branch, utf8_branch) 510 end 511 end 512 513 describe "#refs_hash" do 514 subject { repository.refs_hash } 515 516 it "has as many entries as branches and tags" do 517 expected_refs = SeedRepo::Repo::BRANCHES + SeedRepo::Repo::TAGS 518 # We flatten in case a commit is pointed at by more than one branch and/or tag 519 expect(subject.values.flatten.size).to eq(expected_refs.size) 520 end 521 522 it 'has valid commit ids as keys' do 523 expect(subject.keys).to all( match(Commit::COMMIT_SHA_PATTERN) ) 524 end 525 526 it 'does not error when dereferenced_target is nil' do 527 blob_id = repository.blob_at('master', 'README.md').id 528 repository_rugged.tags.create("refs/tags/blob-tag", blob_id) 529 530 expect { subject }.not_to raise_error 531 end 532 end 533 534 describe '#fetch_remote' do 535 let(:url) { 'http://example.clom' } 536 537 it 'delegates to the gitaly RepositoryService' do 538 ssh_auth = double(:ssh_auth) 539 expected_opts = { 540 ssh_auth: ssh_auth, 541 forced: true, 542 no_tags: true, 543 timeout: described_class::GITLAB_PROJECTS_TIMEOUT, 544 prune: false, 545 check_tags_changed: false, 546 refmap: nil, 547 http_authorization_header: "" 548 } 549 550 expect(repository.gitaly_repository_client).to receive(:fetch_remote).with(url, expected_opts) 551 552 repository.fetch_remote(url, ssh_auth: ssh_auth, forced: true, no_tags: true, prune: false, check_tags_changed: false) 553 end 554 555 it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RepositoryService, :fetch_remote do 556 subject { repository.fetch_remote(url) } 557 end 558 end 559 560 describe '#search_files_by_content' do 561 let(:repository) { mutable_repository } 562 let(:repository_rugged) { mutable_repository_rugged } 563 let(:ref) { 'search-files-by-content-branch' } 564 let(:content) { 'foobarbazmepmep' } 565 566 before do 567 repository.create_branch(ref) 568 new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', ref, 'committing something', content) 569 new_commit_edit_new_file_on_branch(repository_rugged, 'anotherfile', ref, 'committing something', content) 570 end 571 572 after do 573 ensure_seeds 574 end 575 576 subject do 577 repository.search_files_by_content(content, ref) 578 end 579 580 it 'has 2 items' do 581 expect(subject.size).to eq(2) 582 end 583 584 it 'has the correct matching line' do 585 expect(subject).to contain_exactly("#{ref}:encoding/CHANGELOG\u00001\u0000#{content}\n", 586 "#{ref}:anotherfile\u00001\u0000#{content}\n") 587 end 588 end 589 590 describe '#search_files_by_regexp' do 591 let(:ref) { 'master' } 592 593 subject(:result) { mutable_repository.search_files_by_regexp(filter, ref) } 594 595 context 'when sending a valid regexp' do 596 let(:filter) { 'files\/.*\/.*\.rb' } 597 598 it 'returns matched files' do 599 expect(result).to contain_exactly('files/links/regex.rb', 600 'files/ruby/popen.rb', 601 'files/ruby/regex.rb', 602 'files/ruby/version_info.rb') 603 end 604 end 605 606 context 'when sending an ivalid regexp' do 607 let(:filter) { '*.rb' } 608 609 it 'raises error' do 610 expect { result }.to raise_error(GRPC::InvalidArgument, 611 /missing argument to repetition operator: `*`/) 612 end 613 end 614 615 context "when the ref doesn't exist" do 616 let(:filter) { 'files\/.*\/.*\.rb' } 617 let(:ref) { 'non-existing-branch' } 618 619 it 'returns an empty array' do 620 expect(result).to eq([]) 621 end 622 end 623 end 624 625 describe '#find_remote_root_ref' do 626 it 'gets the remote root ref from GitalyClient' do 627 expect_any_instance_of(Gitlab::GitalyClient::RemoteService) 628 .to receive(:find_remote_root_ref).and_call_original 629 630 expect(repository.find_remote_root_ref(SeedHelper::GITLAB_GIT_TEST_REPO_URL)).to eq 'master' 631 end 632 633 it 'returns UTF-8' do 634 expect(repository.find_remote_root_ref(SeedHelper::GITLAB_GIT_TEST_REPO_URL)).to be_utf8 635 end 636 637 it 'returns nil when remote name is nil' do 638 expect_any_instance_of(Gitlab::GitalyClient::RemoteService) 639 .not_to receive(:find_remote_root_ref) 640 641 expect(repository.find_remote_root_ref(nil)).to be_nil 642 end 643 644 it 'returns nil when remote name is empty' do 645 expect_any_instance_of(Gitlab::GitalyClient::RemoteService) 646 .not_to receive(:find_remote_root_ref) 647 648 expect(repository.find_remote_root_ref('')).to be_nil 649 end 650 651 it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RemoteService, :find_remote_root_ref do 652 subject { repository.find_remote_root_ref(SeedHelper::GITLAB_GIT_TEST_REPO_URL) } 653 end 654 end 655 656 describe "#log" do 657 shared_examples 'repository log' do 658 let(:commit_with_old_name) do 659 Gitlab::Git::Commit.find(repository, @commit_with_old_name_id) 660 end 661 662 let(:commit_with_new_name) do 663 Gitlab::Git::Commit.find(repository, @commit_with_new_name_id) 664 end 665 666 let(:rename_commit) do 667 Gitlab::Git::Commit.find(repository, @rename_commit_id) 668 end 669 670 before do 671 # Add new commits so that there's a renamed file in the commit history 672 @commit_with_old_name_id = new_commit_edit_old_file(repository_rugged).oid 673 @rename_commit_id = new_commit_move_file(repository_rugged).oid 674 @commit_with_new_name_id = new_commit_edit_new_file(repository_rugged, "encoding/CHANGELOG", "Edit encoding/CHANGELOG", "I'm a new changelog with different text").oid 675 end 676 677 after do 678 # Erase our commits so other tests get the original repo 679 repository_rugged.references.update("refs/heads/master", SeedRepo::LastCommit::ID) 680 end 681 682 context "where 'follow' == true" do 683 let(:options) { { ref: "master", follow: true } } 684 685 context "and 'path' is a directory" do 686 it "does not follow renames" do 687 log_commits = repository.log(options.merge(path: "encoding")) 688 689 aggregate_failures do 690 expect(log_commits).to include(commit_with_new_name) 691 expect(log_commits).to include(rename_commit) 692 expect(log_commits).not_to include(commit_with_old_name) 693 end 694 end 695 end 696 697 context "and 'path' is a file that matches the new filename" do 698 context 'without offset' do 699 it "follows renames" do 700 log_commits = repository.log(options.merge(path: "encoding/CHANGELOG")) 701 702 aggregate_failures do 703 expect(log_commits).to include(commit_with_new_name) 704 expect(log_commits).to include(rename_commit) 705 expect(log_commits).to include(commit_with_old_name) 706 end 707 end 708 end 709 710 context 'with offset=1' do 711 it "follows renames and skip the latest commit" do 712 log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1)) 713 714 aggregate_failures do 715 expect(log_commits).not_to include(commit_with_new_name) 716 expect(log_commits).to include(rename_commit) 717 expect(log_commits).to include(commit_with_old_name) 718 end 719 end 720 end 721 722 context 'with offset=1', 'and limit=1' do 723 it "follows renames, skip the latest commit and return only one commit" do 724 log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 1)) 725 726 expect(log_commits).to contain_exactly(rename_commit) 727 end 728 end 729 730 context 'with offset=1', 'and limit=2' do 731 it "follows renames, skip the latest commit and return only two commits" do 732 log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 2)) 733 734 aggregate_failures do 735 expect(log_commits).to contain_exactly(rename_commit, commit_with_old_name) 736 end 737 end 738 end 739 740 context 'with offset=2' do 741 it "follows renames and skip the latest commit" do 742 log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2)) 743 744 aggregate_failures do 745 expect(log_commits).not_to include(commit_with_new_name) 746 expect(log_commits).not_to include(rename_commit) 747 expect(log_commits).to include(commit_with_old_name) 748 end 749 end 750 end 751 752 context 'with offset=2', 'and limit=1' do 753 it "follows renames, skip the two latest commit and return only one commit" do 754 log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 1)) 755 756 expect(log_commits).to contain_exactly(commit_with_old_name) 757 end 758 end 759 760 context 'with offset=2', 'and limit=2' do 761 it "follows renames, skip the two latest commit and return only one commit" do 762 log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 2)) 763 764 aggregate_failures do 765 expect(log_commits).not_to include(commit_with_new_name) 766 expect(log_commits).not_to include(rename_commit) 767 expect(log_commits).to include(commit_with_old_name) 768 end 769 end 770 end 771 end 772 773 context "and 'path' is a file that matches the old filename" do 774 it "does not follow renames" do 775 log_commits = repository.log(options.merge(path: "CHANGELOG")) 776 777 aggregate_failures do 778 expect(log_commits).not_to include(commit_with_new_name) 779 expect(log_commits).to include(rename_commit) 780 expect(log_commits).to include(commit_with_old_name) 781 end 782 end 783 end 784 785 context "unknown ref" do 786 it "returns an empty array" do 787 log_commits = repository.log(options.merge(ref: 'unknown')) 788 789 expect(log_commits).to eq([]) 790 end 791 end 792 end 793 794 context "where 'follow' == false" do 795 options = { follow: false } 796 797 context "and 'path' is a directory" do 798 let(:log_commits) do 799 repository.log(options.merge(path: "encoding")) 800 end 801 802 it "does not follow renames" do 803 expect(log_commits).to include(commit_with_new_name) 804 expect(log_commits).to include(rename_commit) 805 expect(log_commits).not_to include(commit_with_old_name) 806 end 807 end 808 809 context "and 'path' is a file that matches the new filename" do 810 let(:log_commits) do 811 repository.log(options.merge(path: "encoding/CHANGELOG")) 812 end 813 814 it "does not follow renames" do 815 expect(log_commits).to include(commit_with_new_name) 816 expect(log_commits).to include(rename_commit) 817 expect(log_commits).not_to include(commit_with_old_name) 818 end 819 end 820 821 context "and 'path' is a file that matches the old filename" do 822 let(:log_commits) do 823 repository.log(options.merge(path: "CHANGELOG")) 824 end 825 826 it "does not follow renames" do 827 expect(log_commits).to include(commit_with_old_name) 828 expect(log_commits).to include(rename_commit) 829 expect(log_commits).not_to include(commit_with_new_name) 830 end 831 end 832 833 context "and 'path' includes a directory that used to be a file" do 834 let(:log_commits) do 835 repository.log(options.merge(ref: "refs/heads/fix-blob-path", path: "files/testdir/file.txt")) 836 end 837 838 it "returns a list of commits" do 839 expect(log_commits.size).to eq(1) 840 end 841 end 842 end 843 844 context "where provides 'after' timestamp" do 845 options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') } 846 847 it "returns commits on or after that timestamp" do 848 commits = repository.log(options) 849 850 expect(commits.size).to be > 0 851 expect(commits).to satisfy do |commits| 852 commits.all? { |commit| commit.committed_date >= options[:after] } 853 end 854 end 855 end 856 857 context "where provides 'before' timestamp" do 858 options = { before: Time.iso8601('2014-03-03T20:15:01+00:00') } 859 860 it "returns commits on or before that timestamp" do 861 commits = repository.log(options) 862 863 expect(commits.size).to be > 0 864 expect(commits).to satisfy do |commits| 865 commits.all? { |commit| commit.committed_date <= options[:before] } 866 end 867 end 868 end 869 870 context 'when multiple paths are provided' do 871 let(:options) { { ref: 'master', path: ['PROCESS.md', 'README.md'] } } 872 873 def commit_files(commit) 874 Gitlab::GitalyClient::StorageSettings.allow_disk_access do 875 commit.deltas.flat_map do |delta| 876 [delta.old_path, delta.new_path].uniq.compact 877 end 878 end 879 end 880 881 it 'only returns commits matching at least one path' do 882 commits = repository.log(options) 883 884 expect(commits.size).to be > 0 885 expect(commits).to satisfy do |commits| 886 commits.none? { |commit| (commit_files(commit) & options[:path]).empty? } 887 end 888 end 889 end 890 891 context 'limit validation' do 892 where(:limit) do 893 [0, nil, '', 'foo'] 894 end 895 896 with_them do 897 it { expect { repository.log(limit: limit) }.to raise_error(ArgumentError) } 898 end 899 end 900 901 context 'with all' do 902 it 'returns a list of commits' do 903 commits = repository.log({ all: true, limit: 50 }) 904 905 expect(commits.size).to eq(37) 906 end 907 end 908 end 909 910 context 'when Gitaly find_commits feature is enabled' do 911 it_behaves_like 'repository log' 912 end 913 end 914 915 describe '#blobs' do 916 let_it_be(:commit_oid) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' } 917 918 shared_examples 'a blob enumeration' do 919 it 'enumerates blobs' do 920 blobs = repository.blobs(revisions).to_a 921 922 expect(blobs.size).to eq(expected_blobs) 923 blobs.each do |blob| 924 expect(blob.data).to be_empty 925 expect(blob.id.size).to be(40) 926 end 927 end 928 end 929 930 context 'single revision' do 931 let(:revisions) { [commit_oid] } 932 let(:expected_blobs) { 53 } 933 934 it_behaves_like 'a blob enumeration' 935 end 936 937 context 'multiple revisions' do 938 let(:revisions) { ["^#{commit_oid}~", commit_oid] } 939 let(:expected_blobs) { 1 } 940 941 it_behaves_like 'a blob enumeration' 942 end 943 944 context 'pseudo revisions' do 945 let(:revisions) { ['master', '--not', '--all'] } 946 let(:expected_blobs) { 0 } 947 948 it_behaves_like 'a blob enumeration' 949 end 950 951 context 'blank revisions' do 952 let(:revisions) { [::Gitlab::Git::BLANK_SHA] } 953 let(:expected_blobs) { 0 } 954 955 before do 956 expect_any_instance_of(Gitlab::GitalyClient::BlobService) 957 .not_to receive(:list_blobs) 958 end 959 960 it_behaves_like 'a blob enumeration' 961 end 962 963 context 'partially blank revisions' do 964 let(:revisions) { [::Gitlab::Git::BLANK_SHA, commit_oid] } 965 let(:expected_blobs) { 53 } 966 967 before do 968 expect_next_instance_of(Gitlab::GitalyClient::BlobService) do |service| 969 expect(service) 970 .to receive(:list_blobs) 971 .with([commit_oid], kind_of(Hash)) 972 .and_call_original 973 end 974 end 975 976 it_behaves_like 'a blob enumeration' 977 end 978 end 979 980 describe '#new_blobs' do 981 let(:repository) { mutable_repository } 982 let(:repository_rugged) { mutable_repository_rugged } 983 let(:blob) { create_blob('This is a new blob') } 984 let(:commit) { create_commit('nested/new-blob.txt' => blob) } 985 986 def create_blob(content) 987 repository_rugged.write(content, :blob) 988 end 989 990 def create_commit(blobs) 991 author = { name: 'Test User', email: 'mail@example.com', time: Time.now } 992 993 index = repository_rugged.index 994 blobs.each do |path, oid| 995 index.add(path: path, oid: oid, mode: 0100644) 996 end 997 998 Rugged::Commit.create(repository_rugged, 999 author: author, 1000 committer: author, 1001 message: "Message", 1002 parents: [], 1003 tree: index.write_tree(repository_rugged)) 1004 end 1005 1006 subject { repository.new_blobs(newrevs).to_a } 1007 1008 shared_examples '#new_blobs with revisions' do 1009 before do 1010 expect_next_instance_of(Gitlab::GitalyClient::BlobService) do |service| 1011 expect(service) 1012 .to receive(:list_blobs) 1013 .with(expected_newrevs, 1014 limit: Gitlab::Git::Repository::REV_LIST_COMMIT_LIMIT, 1015 with_paths: true, 1016 dynamic_timeout: nil) 1017 .once 1018 .and_call_original 1019 end 1020 end 1021 1022 it 'enumerates new blobs' do 1023 expect(subject).to match_array(expected_blobs) 1024 end 1025 1026 it 'memoizes results' do 1027 expect(subject).to match_array(expected_blobs) 1028 expect(subject).to match_array(expected_blobs) 1029 end 1030 end 1031 1032 context 'with a single revision' do 1033 let(:newrevs) { commit } 1034 let(:expected_newrevs) { ['--not', '--all', '--not', newrevs] } 1035 let(:expected_blobs) do 1036 [have_attributes(class: Gitlab::Git::Blob, id: blob, path: 'nested/new-blob.txt', size: 18)] 1037 end 1038 1039 it_behaves_like '#new_blobs with revisions' 1040 end 1041 1042 context 'with a single-entry array' do 1043 let(:newrevs) { [commit] } 1044 let(:expected_newrevs) { ['--not', '--all', '--not'] + newrevs } 1045 let(:expected_blobs) do 1046 [have_attributes(class: Gitlab::Git::Blob, id: blob, path: 'nested/new-blob.txt', size: 18)] 1047 end 1048 1049 it_behaves_like '#new_blobs with revisions' 1050 end 1051 1052 context 'with multiple revisions' do 1053 let(:another_blob) { create_blob('Another blob') } 1054 let(:newrevs) { [commit, create_commit('another_path.txt' => another_blob)] } 1055 let(:expected_newrevs) { ['--not', '--all', '--not'] + newrevs.sort } 1056 let(:expected_blobs) do 1057 [ 1058 have_attributes(class: Gitlab::Git::Blob, id: blob, path: 'nested/new-blob.txt', size: 18), 1059 have_attributes(class: Gitlab::Git::Blob, id: another_blob, path: 'another_path.txt', size: 12) 1060 ] 1061 end 1062 1063 it_behaves_like '#new_blobs with revisions' 1064 end 1065 1066 context 'with partially blank revisions' do 1067 let(:newrevs) { [nil, commit, Gitlab::Git::BLANK_SHA] } 1068 let(:expected_newrevs) { ['--not', '--all', '--not', commit] } 1069 let(:expected_blobs) do 1070 [ 1071 have_attributes(class: Gitlab::Git::Blob, id: blob, path: 'nested/new-blob.txt', size: 18) 1072 ] 1073 end 1074 1075 it_behaves_like '#new_blobs with revisions' 1076 end 1077 1078 context 'with repeated revisions' do 1079 let(:newrevs) { [commit, commit, commit] } 1080 let(:expected_newrevs) { ['--not', '--all', '--not', commit] } 1081 let(:expected_blobs) do 1082 [ 1083 have_attributes(class: Gitlab::Git::Blob, id: blob, path: 'nested/new-blob.txt', size: 18) 1084 ] 1085 end 1086 1087 it_behaves_like '#new_blobs with revisions' 1088 end 1089 1090 context 'with preexisting commits' do 1091 let(:newrevs) { ['refs/heads/master'] } 1092 let(:expected_newrevs) { ['--not', '--all', '--not'] + newrevs } 1093 let(:expected_blobs) { [] } 1094 1095 it_behaves_like '#new_blobs with revisions' 1096 end 1097 1098 shared_examples '#new_blobs without revisions' do 1099 before do 1100 expect(Gitlab::GitalyClient::BlobService).not_to receive(:new) 1101 end 1102 1103 it 'returns an empty array' do 1104 expect(subject).to eq([]) 1105 end 1106 end 1107 1108 context 'with a single nil newrev' do 1109 let(:newrevs) { nil } 1110 1111 it_behaves_like '#new_blobs without revisions' 1112 end 1113 1114 context 'with a single zero newrev' do 1115 let(:newrevs) { Gitlab::Git::BLANK_SHA } 1116 1117 it_behaves_like '#new_blobs without revisions' 1118 end 1119 1120 context 'with an empty array' do 1121 let(:newrevs) { [] } 1122 1123 it_behaves_like '#new_blobs without revisions' 1124 end 1125 1126 context 'with array containing only empty refs' do 1127 let(:newrevs) { [nil, Gitlab::Git::BLANK_SHA] } 1128 1129 it_behaves_like '#new_blobs without revisions' 1130 end 1131 end 1132 1133 describe '#new_commits' do 1134 let(:repository) { mutable_repository } 1135 let(:new_commit) do 1136 author = { name: 'Test User', email: 'mail@example.com', time: Time.now } 1137 1138 Rugged::Commit.create(repository_rugged, 1139 author: author, 1140 committer: author, 1141 message: "Message", 1142 parents: [], 1143 tree: "4b825dc642cb6eb9a060e54bf8d69288fbee4904") 1144 end 1145 1146 let(:expected_commits) { 1 } 1147 let(:revisions) { [new_commit] } 1148 1149 before do 1150 expect_next_instance_of(Gitlab::GitalyClient::CommitService) do |service| 1151 expect(service) 1152 .to receive(:list_commits) 1153 .with([new_commit, '--not', '--all']) 1154 .and_call_original 1155 end 1156 end 1157 1158 it 'enumerates commits' do 1159 commits = repository.new_commits(revisions).to_a 1160 1161 expect(commits.size).to eq(expected_commits) 1162 commits.each do |commit| 1163 expect(commit.id).to eq(new_commit) 1164 expect(commit.message).to eq("Message") 1165 end 1166 end 1167 end 1168 1169 describe '#count_commits_between' do 1170 subject { repository.count_commits_between('feature', 'master') } 1171 1172 it { is_expected.to eq(17) } 1173 end 1174 1175 describe '#raw_changes_between' do 1176 let(:old_rev) { } 1177 let(:new_rev) { } 1178 let(:changes) { repository.raw_changes_between(old_rev, new_rev) } 1179 1180 context 'initial commit' do 1181 let(:old_rev) { Gitlab::Git::BLANK_SHA } 1182 let(:new_rev) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' } 1183 1184 it 'returns the changes' do 1185 expect(changes).to be_present 1186 expect(changes.size).to eq(3) 1187 end 1188 end 1189 1190 context 'with an invalid rev' do 1191 let(:old_rev) { 'foo' } 1192 let(:new_rev) { 'bar' } 1193 1194 it 'returns an error' do 1195 expect { changes }.to raise_error(Gitlab::Git::Repository::GitError) 1196 end 1197 end 1198 1199 context 'with valid revs' do 1200 let(:old_rev) { 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6' } 1201 let(:new_rev) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' } 1202 1203 it 'returns the changes' do 1204 expect(changes.size).to eq(9) 1205 expect(changes.first.operation).to eq(:modified) 1206 expect(changes.first.new_path).to eq('.gitmodules') 1207 expect(changes.last.operation).to eq(:added) 1208 expect(changes.last.new_path).to eq('files/lfs/picture-invalid.png') 1209 end 1210 end 1211 end 1212 1213 describe '#merge_base' do 1214 where(:from, :to, :result) do 1215 '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' | '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' 1216 '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' 1217 '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | 'foobar' | nil 1218 'foobar' | '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | nil 1219 end 1220 1221 with_them do 1222 it { expect(repository.merge_base(from, to)).to eq(result) } 1223 end 1224 end 1225 1226 describe '#count_commits' do 1227 describe 'extended commit counting' do 1228 context 'with after timestamp' do 1229 it 'returns the number of commits after timestamp' do 1230 options = { ref: 'master', after: Time.iso8601('2013-03-03T20:15:01+00:00') } 1231 1232 expect(repository.count_commits(options)).to eq(25) 1233 end 1234 end 1235 1236 context 'with before timestamp' do 1237 it 'returns the number of commits before timestamp' do 1238 options = { ref: 'feature', before: Time.iso8601('2015-03-03T20:15:01+00:00') } 1239 1240 expect(repository.count_commits(options)).to eq(9) 1241 end 1242 end 1243 1244 context 'with max_count' do 1245 it 'returns the number of commits with path' do 1246 options = { ref: 'master', max_count: 5 } 1247 1248 expect(repository.count_commits(options)).to eq(5) 1249 end 1250 end 1251 1252 context 'with path' do 1253 it 'returns the number of commits with path' do 1254 options = { ref: 'master', path: 'encoding' } 1255 1256 expect(repository.count_commits(options)).to eq(2) 1257 end 1258 end 1259 1260 context 'with option :from and option :to' do 1261 it 'returns the number of commits ahead for fix-mode..fix-blob-path' do 1262 options = { from: 'fix-mode', to: 'fix-blob-path' } 1263 1264 expect(repository.count_commits(options)).to eq(2) 1265 end 1266 1267 it 'returns the number of commits ahead for fix-blob-path..fix-mode' do 1268 options = { from: 'fix-blob-path', to: 'fix-mode' } 1269 1270 expect(repository.count_commits(options)).to eq(1) 1271 end 1272 1273 context 'with option :left_right' do 1274 it 'returns the number of commits for fix-mode...fix-blob-path' do 1275 options = { from: 'fix-mode', to: 'fix-blob-path', left_right: true } 1276 1277 expect(repository.count_commits(options)).to eq([1, 2]) 1278 end 1279 1280 context 'with max_count' do 1281 it 'returns the number of commits with path' do 1282 options = { from: 'fix-mode', to: 'fix-blob-path', left_right: true, max_count: 1 } 1283 1284 expect(repository.count_commits(options)).to eq([1, 1]) 1285 end 1286 end 1287 end 1288 end 1289 1290 context 'with max_count' do 1291 it 'returns the number of commits up to the passed limit' do 1292 options = { ref: 'master', max_count: 10, after: Time.iso8601('2013-03-03T20:15:01+00:00') } 1293 1294 expect(repository.count_commits(options)).to eq(10) 1295 end 1296 end 1297 1298 context "with all" do 1299 it "returns the number of commits in the whole repository" do 1300 options = { all: true } 1301 1302 expect(repository.count_commits(options)).to eq(34) 1303 end 1304 end 1305 1306 context 'without all or ref being specified' do 1307 it "raises an ArgumentError" do 1308 expect { repository.count_commits({}) }.to raise_error(ArgumentError) 1309 end 1310 end 1311 end 1312 end 1313 1314 describe '#find_branch' do 1315 it 'returns a Branch for master' do 1316 branch = repository.find_branch('master') 1317 1318 expect(branch).to be_a_kind_of(Gitlab::Git::Branch) 1319 expect(branch.name).to eq('master') 1320 end 1321 1322 it 'handles non-existent branch' do 1323 branch = repository.find_branch('this-is-garbage') 1324 1325 expect(branch).to eq(nil) 1326 end 1327 end 1328 1329 describe '#branches' do 1330 subject { repository.branches } 1331 1332 context 'with local and remote branches' do 1333 let(:repository) { mutable_repository } 1334 1335 before do 1336 create_remote_branch('joe', 'remote_branch', 'master') 1337 repository.create_branch('local_branch') 1338 end 1339 1340 after do 1341 ensure_seeds 1342 end 1343 1344 it 'returns the local and remote branches' do 1345 expect(subject.any? { |b| b.name == 'joe/remote_branch' }).to eq(true) 1346 expect(subject.any? { |b| b.name == 'local_branch' }).to eq(true) 1347 end 1348 end 1349 1350 it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :branches 1351 end 1352 1353 describe '#branch_count' do 1354 it 'returns the number of branches' do 1355 expect(repository.branch_count).to eq(11) 1356 end 1357 1358 context 'with local and remote branches' do 1359 let(:repository) { mutable_repository } 1360 1361 before do 1362 create_remote_branch('joe', 'remote_branch', 'master') 1363 repository.create_branch('local_branch') 1364 end 1365 1366 after do 1367 ensure_seeds 1368 end 1369 1370 it 'returns the count of local branches' do 1371 expect(repository.branch_count).to eq(repository.local_branches.count) 1372 end 1373 1374 context 'with Gitaly disabled' do 1375 before do 1376 allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false) 1377 end 1378 1379 it 'returns the count of local branches' do 1380 expect(repository.branch_count).to eq(repository.local_branches.count) 1381 end 1382 end 1383 end 1384 end 1385 1386 describe '#merged_branch_names' do 1387 context 'when branch names are passed' do 1388 it 'only returns the names we are asking' do 1389 names = repository.merged_branch_names(%w[merge-test]) 1390 1391 expect(names).to contain_exactly('merge-test') 1392 end 1393 1394 it 'does not return unmerged branch names' do 1395 names = repository.merged_branch_names(%w[feature]) 1396 1397 expect(names).to be_empty 1398 end 1399 end 1400 1401 context 'when no root ref is available' do 1402 it 'returns empty list' do 1403 project = create(:project, :empty_repo) 1404 1405 names = project.repository.merged_branch_names(%w[feature]) 1406 1407 expect(names).to be_empty 1408 end 1409 end 1410 1411 context 'when no branch names are specified' do 1412 before do 1413 repository.create_branch('identical') 1414 end 1415 1416 after do 1417 ensure_seeds 1418 end 1419 1420 it 'returns all merged branch names except for identical one' do 1421 names = repository.merged_branch_names 1422 1423 expect(names).to include('merge-test') 1424 expect(names).to include('fix-mode') 1425 expect(names).not_to include('feature') 1426 expect(names).not_to include('identical') 1427 end 1428 end 1429 end 1430 1431 describe '#diff_stats' do 1432 let(:left_commit_id) { 'feature' } 1433 let(:right_commit_id) { 'master' } 1434 1435 it 'returns a DiffStatsCollection' do 1436 collection = repository.diff_stats(left_commit_id, right_commit_id) 1437 1438 expect(collection).to be_a(Gitlab::Git::DiffStatsCollection) 1439 expect(collection).to be_a(Enumerable) 1440 end 1441 1442 it 'yields Gitaly::DiffStats objects' do 1443 collection = repository.diff_stats(left_commit_id, right_commit_id) 1444 1445 expect(collection.to_a).to all(be_a(Gitaly::DiffStats)) 1446 end 1447 1448 it 'returns no Gitaly::DiffStats when SHAs are invalid' do 1449 collection = repository.diff_stats('foo', 'bar') 1450 1451 expect(collection).to be_a(Gitlab::Git::DiffStatsCollection) 1452 expect(collection).to be_a(Enumerable) 1453 expect(collection.to_a).to be_empty 1454 end 1455 1456 it 'returns no Gitaly::DiffStats when there is a nil SHA' do 1457 expect_any_instance_of(Gitlab::GitalyClient::CommitService) 1458 .not_to receive(:diff_stats) 1459 1460 collection = repository.diff_stats(nil, 'master') 1461 1462 expect(collection).to be_a(Gitlab::Git::DiffStatsCollection) 1463 expect(collection).to be_a(Enumerable) 1464 expect(collection.to_a).to be_empty 1465 end 1466 1467 it 'returns no Gitaly::DiffStats when there is a BLANK_SHA' do 1468 expect_any_instance_of(Gitlab::GitalyClient::CommitService) 1469 .not_to receive(:diff_stats) 1470 1471 collection = repository.diff_stats(Gitlab::Git::BLANK_SHA, 'master') 1472 1473 expect(collection).to be_a(Gitlab::Git::DiffStatsCollection) 1474 expect(collection).to be_a(Enumerable) 1475 expect(collection.to_a).to be_empty 1476 end 1477 end 1478 1479 describe '#find_changed_paths' do 1480 let(:commit_1) { 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6' } 1481 let(:commit_2) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' } 1482 let(:commit_3) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' } 1483 let(:commit_1_files) do 1484 [ 1485 Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/executables/ls"), 1486 Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/executables/touch"), 1487 Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/links/regex.rb"), 1488 Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/links/ruby-style-guide.md"), 1489 Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/links/touch"), 1490 Gitlab::Git::ChangedPath.new(status: :MODIFIED, path: ".gitmodules"), 1491 Gitlab::Git::ChangedPath.new(status: :ADDED, path: "deeper/nested/six"), 1492 Gitlab::Git::ChangedPath.new(status: :ADDED, path: "nested/six") 1493 ] 1494 end 1495 1496 let(:commit_2_files) do 1497 [Gitlab::Git::ChangedPath.new(status: :ADDED, path: "bin/executable")] 1498 end 1499 1500 let(:commit_3_files) do 1501 [ 1502 Gitlab::Git::ChangedPath.new(status: :MODIFIED, path: ".gitmodules"), 1503 Gitlab::Git::ChangedPath.new(status: :ADDED, path: "gitlab-shell") 1504 ] 1505 end 1506 1507 it 'returns a list of paths' do 1508 collection = repository.find_changed_paths([commit_1, commit_2, commit_3]) 1509 1510 expect(collection).to be_a(Enumerable) 1511 expect(collection.as_json).to eq((commit_1_files + commit_2_files + commit_3_files).as_json) 1512 end 1513 1514 it 'returns no paths when SHAs are invalid' do 1515 collection = repository.find_changed_paths(['invalid', commit_1]) 1516 1517 expect(collection).to be_a(Enumerable) 1518 expect(collection.to_a).to be_empty 1519 end 1520 1521 it 'returns a list of paths even when containing a blank ref' do 1522 collection = repository.find_changed_paths([nil, commit_1]) 1523 1524 expect(collection).to be_a(Enumerable) 1525 expect(collection.as_json).to eq(commit_1_files.as_json) 1526 end 1527 1528 it 'returns no paths when the commits are nil' do 1529 expect_any_instance_of(Gitlab::GitalyClient::CommitService) 1530 .not_to receive(:find_changed_paths) 1531 1532 collection = repository.find_changed_paths([nil, nil]) 1533 1534 expect(collection).to be_a(Enumerable) 1535 expect(collection.to_a).to be_empty 1536 end 1537 end 1538 1539 describe "#ls_files" do 1540 let(:master_file_paths) { repository.ls_files("master") } 1541 let(:utf8_file_paths) { repository.ls_files("ls-files-utf8") } 1542 let(:not_existed_branch) { repository.ls_files("not_existed_branch") } 1543 1544 it "read every file paths of master branch" do 1545 expect(master_file_paths.length).to equal(40) 1546 end 1547 1548 it "reads full file paths of master branch" do 1549 expect(master_file_paths).to include("files/html/500.html") 1550 end 1551 1552 it "does not read submodule directory and empty directory of master branch" do 1553 expect(master_file_paths).not_to include("six") 1554 end 1555 1556 it "does not include 'nil'" do 1557 expect(master_file_paths).not_to include(nil) 1558 end 1559 1560 it "returns empty array when not existed branch" do 1561 expect(not_existed_branch.length).to equal(0) 1562 end 1563 1564 it "returns valid utf-8 data" do 1565 expect(utf8_file_paths.map { |file| file.force_encoding('utf-8') }).to all(be_valid_encoding) 1566 end 1567 end 1568 1569 describe "#copy_gitattributes" do 1570 let(:attributes_path) { File.join(SEED_STORAGE_PATH, TEST_REPO_PATH, 'info/attributes') } 1571 1572 after do 1573 FileUtils.rm_rf(attributes_path) if Dir.exist?(attributes_path) 1574 end 1575 1576 it "raises an error with invalid ref" do 1577 expect { repository.copy_gitattributes("invalid") }.to raise_error(Gitlab::Git::Repository::InvalidRef) 1578 end 1579 1580 context 'when forcing encoding issues' do 1581 let(:branch_name) { "ʕ•ᴥ•ʔ" } 1582 1583 before do 1584 repository.create_branch(branch_name) 1585 end 1586 1587 after do 1588 repository.rm_branch(branch_name, user: build(:admin)) 1589 end 1590 1591 it "doesn't raise with a valid unicode ref" do 1592 expect { repository.copy_gitattributes(branch_name) }.not_to raise_error 1593 1594 repository 1595 end 1596 end 1597 1598 context "with no .gitattrbutes" do 1599 before do 1600 repository.copy_gitattributes("master") 1601 end 1602 1603 it "does not have an info/attributes" do 1604 expect(File.exist?(attributes_path)).to be_falsey 1605 end 1606 end 1607 1608 context "with .gitattrbutes" do 1609 before do 1610 repository.copy_gitattributes("gitattributes") 1611 end 1612 1613 it "has an info/attributes" do 1614 expect(File.exist?(attributes_path)).to be_truthy 1615 end 1616 1617 it "has the same content in info/attributes as .gitattributes" do 1618 contents = File.open(attributes_path, "rb") { |f| f.read } 1619 expect(contents).to eq("*.md binary\n") 1620 end 1621 end 1622 1623 context "with updated .gitattrbutes" do 1624 before do 1625 repository.copy_gitattributes("gitattributes") 1626 repository.copy_gitattributes("gitattributes-updated") 1627 end 1628 1629 it "has an info/attributes" do 1630 expect(File.exist?(attributes_path)).to be_truthy 1631 end 1632 1633 it "has the updated content in info/attributes" do 1634 contents = File.read(attributes_path) 1635 expect(contents).to eq("*.txt binary\n") 1636 end 1637 end 1638 1639 context "with no .gitattrbutes in HEAD but with previous info/attributes" do 1640 before do 1641 repository.copy_gitattributes("gitattributes") 1642 repository.copy_gitattributes("master") 1643 end 1644 1645 it "does not have an info/attributes" do 1646 expect(File.exist?(attributes_path)).to be_falsey 1647 end 1648 end 1649 end 1650 1651 describe '#gitattribute' do 1652 let(:repository) { Gitlab::Git::Repository.new('default', TEST_GITATTRIBUTES_REPO_PATH, '', 'group/project') } 1653 1654 after do 1655 ensure_seeds 1656 end 1657 1658 it 'returns matching language attribute' do 1659 expect(repository.gitattribute("custom-highlighting/test.gitlab-custom", 'gitlab-language')).to eq('ruby') 1660 end 1661 1662 it 'returns matching language attribute with additional options' do 1663 expect(repository.gitattribute("custom-highlighting/test.gitlab-cgi", 'gitlab-language')).to eq('erb?parent=json') 1664 end 1665 1666 it 'returns nil if nothing matches' do 1667 expect(repository.gitattribute("report.xslt", 'gitlab-language')).to eq(nil) 1668 end 1669 1670 context 'without gitattributes file' do 1671 let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } 1672 1673 it 'returns nil' do 1674 expect(repository.gitattribute("README.md", 'gitlab-language')).to eq(nil) 1675 end 1676 end 1677 end 1678 1679 describe '#ref_exists?' do 1680 it 'returns true for an existing tag' do 1681 expect(repository.ref_exists?('refs/heads/master')).to eq(true) 1682 end 1683 1684 it 'returns false for a non-existing tag' do 1685 expect(repository.ref_exists?('refs/tags/THIS_TAG_DOES_NOT_EXIST')).to eq(false) 1686 end 1687 1688 it 'raises an ArgumentError for an empty string' do 1689 expect { repository.ref_exists?('') }.to raise_error(ArgumentError) 1690 end 1691 1692 it 'raises an ArgumentError for an invalid ref' do 1693 expect { repository.ref_exists?('INVALID') }.to raise_error(ArgumentError) 1694 end 1695 end 1696 1697 describe '#tag_exists?' do 1698 it 'returns true for an existing tag' do 1699 tag = repository.tag_names.first 1700 1701 expect(repository.tag_exists?(tag)).to eq(true) 1702 end 1703 1704 it 'returns false for a non-existing tag' do 1705 expect(repository.tag_exists?('v9000')).to eq(false) 1706 end 1707 end 1708 1709 describe '#branch_exists?' do 1710 it 'returns true for an existing branch' do 1711 expect(repository.branch_exists?('master')).to eq(true) 1712 end 1713 1714 it 'returns false for a non-existing branch' do 1715 expect(repository.branch_exists?('kittens')).to eq(false) 1716 end 1717 1718 it 'returns false when using an invalid branch name' do 1719 expect(repository.branch_exists?('.bla')).to eq(false) 1720 end 1721 end 1722 1723 describe '#local_branches' do 1724 let(:repository) { mutable_repository } 1725 1726 before do 1727 create_remote_branch('joe', 'remote_branch', 'master') 1728 repository.create_branch('local_branch') 1729 end 1730 1731 after do 1732 ensure_seeds 1733 end 1734 1735 it 'returns the local branches' do 1736 expect(repository.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false) 1737 expect(repository.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true) 1738 end 1739 1740 it 'returns a Branch with UTF-8 fields' do 1741 branches = repository.local_branches.to_a 1742 expect(branches.size).to be > 0 1743 branches.each do |branch| 1744 expect(branch.name).to be_utf8 1745 expect(branch.target).to be_utf8 unless branch.target.nil? 1746 end 1747 end 1748 1749 it 'gets the branches from GitalyClient' do 1750 expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:local_branches) 1751 .and_return([]) 1752 repository.local_branches 1753 end 1754 1755 it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :local_branches do 1756 subject { repository.local_branches } 1757 end 1758 end 1759 1760 describe '#languages' do 1761 it 'returns exactly the expected results' do 1762 languages = repository.languages('4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6') 1763 expected_languages = [ 1764 { value: 66.63, label: "Ruby", color: "#701516", highlight: "#701516" }, 1765 { value: 22.96, label: "JavaScript", color: "#f1e05a", highlight: "#f1e05a" }, 1766 { value: 7.9, label: "HTML", color: "#e34c26", highlight: "#e34c26" }, 1767 { value: 2.51, label: "CoffeeScript", color: "#244776", highlight: "#244776" } 1768 ] 1769 1770 expect(languages.size).to eq(expected_languages.size) 1771 1772 expected_languages.size.times do |i| 1773 a = expected_languages[i] 1774 b = languages[i] 1775 1776 expect(a.keys.sort).to eq(b.keys.sort) 1777 expect(a[:value]).to be_within(0.1).of(b[:value]) 1778 1779 non_float_keys = a.keys - [:value] 1780 expect(a.values_at(*non_float_keys)).to eq(b.values_at(*non_float_keys)) 1781 end 1782 end 1783 1784 it "uses the repository's HEAD when no ref is passed" do 1785 lang = repository.languages.first 1786 1787 expect(lang[:label]).to eq('Ruby') 1788 end 1789 end 1790 1791 describe '#license_short_name' do 1792 subject { repository.license_short_name } 1793 1794 context 'when no license file can be found' do 1795 let(:project) { create(:project, :repository) } 1796 let(:repository) { project.repository.raw_repository } 1797 1798 before do 1799 project.repository.delete_file(project.owner, 'LICENSE', message: 'remove license', branch_name: 'master') 1800 end 1801 1802 it { is_expected.to be_nil } 1803 end 1804 1805 context 'when an mit license is found' do 1806 it { is_expected.to eq('mit') } 1807 end 1808 end 1809 1810 describe '#fetch_source_branch!' do 1811 let(:local_ref) { 'refs/merge-requests/1/head' } 1812 let(:source_repository) { mutable_repository } 1813 1814 after do 1815 ensure_seeds 1816 end 1817 1818 context 'when the branch exists' do 1819 context 'when the commit does not exist locally' do 1820 let(:source_branch) { 'new-branch-for-fetch-source-branch' } 1821 let(:source_path) { File.join(TestEnv.repos_path, source_repository.relative_path) } 1822 let(:source_rugged) { Rugged::Repository.new(source_path) } 1823 let(:new_oid) { new_commit_edit_old_file(source_rugged).oid } 1824 1825 before do 1826 source_rugged.branches.create(source_branch, new_oid) 1827 end 1828 1829 it 'writes the ref' do 1830 expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true) 1831 expect(repository.commit(local_ref).sha).to eq(new_oid) 1832 end 1833 end 1834 1835 context 'when the commit exists locally' do 1836 let(:source_branch) { 'master' } 1837 let(:expected_oid) { SeedRepo::LastCommit::ID } 1838 1839 it 'writes the ref' do 1840 # Sanity check: the commit should already exist 1841 expect(repository.commit(expected_oid)).not_to be_nil 1842 1843 expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true) 1844 expect(repository.commit(local_ref).sha).to eq(expected_oid) 1845 end 1846 end 1847 end 1848 1849 context 'when the branch does not exist' do 1850 let(:source_branch) { 'definitely-not-master' } 1851 1852 it 'does not write the ref' do 1853 expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(false) 1854 expect(repository.commit(local_ref)).to be_nil 1855 end 1856 end 1857 end 1858 1859 describe '#rm_branch' do 1860 let(:project) { create(:project, :repository) } 1861 let(:repository) { project.repository.raw } 1862 let(:branch_name) { "to-be-deleted-soon" } 1863 1864 before do 1865 project.add_developer(user) 1866 repository.create_branch(branch_name) 1867 end 1868 1869 it "removes the branch from the repo" do 1870 repository.rm_branch(branch_name, user: user) 1871 1872 expect(repository_rugged.branches[branch_name]).to be_nil 1873 end 1874 end 1875 1876 describe '#write_ref' do 1877 context 'validations' do 1878 using RSpec::Parameterized::TableSyntax 1879 1880 where(:ref_path, :ref) do 1881 'foo bar' | '123' 1882 'foobar' | "12\x003" 1883 end 1884 1885 with_them do 1886 it 'raises ArgumentError' do 1887 expect { repository.write_ref(ref_path, ref) }.to raise_error(ArgumentError) 1888 end 1889 end 1890 end 1891 1892 it 'writes the HEAD' do 1893 repository.write_ref('HEAD', 'refs/heads/feature') 1894 1895 expect(repository.commit('HEAD')).to eq(repository.commit('feature')) 1896 expect(repository.root_ref).to eq('feature') 1897 end 1898 1899 it 'writes other refs' do 1900 repository.write_ref('refs/heads/feature', SeedRepo::Commit::ID) 1901 1902 expect(repository.commit('feature').sha).to eq(SeedRepo::Commit::ID) 1903 end 1904 end 1905 1906 describe '#list_refs' do 1907 it 'returns a list of branches with their head commit' do 1908 refs = repository.list_refs 1909 reference = refs.first 1910 1911 expect(refs).to be_an(Enumerable) 1912 expect(reference).to be_a(Gitaly::ListRefsResponse::Reference) 1913 expect(reference.name).to be_a(String) 1914 expect(reference.target).to be_a(String) 1915 end 1916 end 1917 1918 describe '#refs_by_oid' do 1919 it 'returns a list of refs from a OID' do 1920 refs = repository.refs_by_oid(oid: repository.commit.id) 1921 1922 expect(refs).to be_an(Array) 1923 expect(refs).to include(Gitlab::Git::BRANCH_REF_PREFIX + repository.root_ref) 1924 end 1925 1926 it 'returns a single ref from a OID' do 1927 refs = repository.refs_by_oid(oid: repository.commit.id, limit: 1) 1928 1929 expect(refs).to be_an(Array) 1930 expect(refs).to eq([Gitlab::Git::BRANCH_REF_PREFIX + repository.root_ref]) 1931 end 1932 1933 it 'returns empty for unknown ID' do 1934 expect(repository.refs_by_oid(oid: Gitlab::Git::BLANK_SHA, limit: 0)).to eq([]) 1935 end 1936 1937 it 'returns nil for an empty repo' do 1938 project = create(:project) 1939 1940 expect(project.repository.refs_by_oid(oid: SeedRepo::Commit::ID, limit: 0)).to be_nil 1941 end 1942 end 1943 1944 describe '#set_full_path' do 1945 before do 1946 repository_rugged.config["gitlab.fullpath"] = repository_path 1947 end 1948 1949 context 'is given a path' do 1950 it 'writes it to disk' do 1951 repository.set_full_path(full_path: "not-the/real-path.git") 1952 1953 config = File.read(File.join(repository_path, "config")) 1954 1955 expect(config).to include("[gitlab]") 1956 expect(config).to include("fullpath = not-the/real-path.git") 1957 end 1958 end 1959 1960 context 'it is given an empty path' do 1961 it 'does not write it to disk' do 1962 repository.set_full_path(full_path: "") 1963 1964 config = File.read(File.join(repository_path, "config")) 1965 1966 expect(config).to include("[gitlab]") 1967 expect(config).to include("fullpath = #{repository_path}") 1968 end 1969 end 1970 1971 context 'repository does not exist' do 1972 it 'raises NoRepository and does not call Gitaly WriteConfig' do 1973 repository = Gitlab::Git::Repository.new('default', 'does/not/exist.git', '', 'group/project') 1974 1975 expect(repository.gitaly_repository_client).not_to receive(:set_full_path) 1976 1977 expect do 1978 repository.set_full_path(full_path: 'foo/bar.git') 1979 end.to raise_error(Gitlab::Git::Repository::NoRepository) 1980 end 1981 end 1982 end 1983 1984 describe '#merge_to_ref' do 1985 let(:repository) { mutable_repository } 1986 let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' } 1987 let(:left_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } 1988 let(:right_branch) { 'test-master' } 1989 let(:first_parent_ref) { 'refs/heads/test-master' } 1990 let(:target_ref) { 'refs/merge-requests/999/merge' } 1991 1992 before do 1993 repository.create_branch(right_branch, branch_head) unless repository.ref_exists?(first_parent_ref) 1994 end 1995 1996 def merge_to_ref 1997 repository.merge_to_ref(user, 1998 source_sha: left_sha, branch: right_branch, target_ref: target_ref, 1999 message: 'Merge message', first_parent_ref: first_parent_ref) 2000 end 2001 2002 it 'generates a commit in the target_ref' do 2003 expect(repository.ref_exists?(target_ref)).to be(false) 2004 2005 commit_sha = merge_to_ref 2006 ref_head = repository.commit(target_ref) 2007 2008 expect(commit_sha).to be_present 2009 expect(repository.ref_exists?(target_ref)).to be(true) 2010 expect(ref_head.id).to eq(commit_sha) 2011 end 2012 2013 it 'does not change the right branch HEAD' do 2014 expect { merge_to_ref }.not_to change { repository.commit(first_parent_ref).sha } 2015 end 2016 end 2017 2018 describe '#merge' do 2019 let(:repository) { mutable_repository } 2020 let(:source_sha) { '913c66a37b4a45b9769037c55c2d238bd0942d2e' } 2021 let(:target_branch) { 'test-merge-target-branch' } 2022 2023 before do 2024 repository.create_branch(target_branch, '6d394385cf567f80a8fd85055db1ab4c5295806f') 2025 end 2026 2027 after do 2028 ensure_seeds 2029 end 2030 2031 it 'can perform a merge' do 2032 merge_commit_id = nil 2033 result = repository.merge(user, source_sha, target_branch, 'Test merge') do |commit_id| 2034 merge_commit_id = commit_id 2035 end 2036 2037 expect(result.newrev).to eq(merge_commit_id) 2038 expect(result.repo_created).to eq(false) 2039 expect(result.branch_created).to eq(false) 2040 end 2041 2042 it 'returns nil if there was a concurrent branch update' do 2043 concurrent_update_id = '33f3729a45c02fc67d00adb1b8bca394b0e761d9' 2044 result = repository.merge(user, source_sha, target_branch, 'Test merge') do 2045 # This ref update should make the merge fail 2046 repository.write_ref(Gitlab::Git::BRANCH_REF_PREFIX + target_branch, concurrent_update_id) 2047 end 2048 2049 # This 'nil' signals that the merge was not applied 2050 expect(result).to be_nil 2051 2052 # Our concurrent ref update should not have been undone 2053 expect(repository.find_branch(target_branch).target).to eq(concurrent_update_id) 2054 end 2055 end 2056 2057 describe '#ff_merge' do 2058 let(:repository) { mutable_repository } 2059 let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' } 2060 let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } 2061 let(:target_branch) { 'test-ff-target-branch' } 2062 2063 before do 2064 repository.create_branch(target_branch, branch_head) 2065 end 2066 2067 after do 2068 ensure_seeds 2069 end 2070 2071 subject { repository.ff_merge(user, source_sha, target_branch) } 2072 2073 shared_examples '#ff_merge' do 2074 it 'performs a ff_merge' do 2075 expect(subject.newrev).to eq(source_sha) 2076 expect(subject.repo_created).to be(false) 2077 expect(subject.branch_created).to be(false) 2078 2079 expect(repository.commit(target_branch).id).to eq(source_sha) 2080 end 2081 2082 context 'with a non-existing target branch' do 2083 subject { repository.ff_merge(user, source_sha, 'this-isnt-real') } 2084 2085 it 'throws an ArgumentError' do 2086 expect { subject }.to raise_error(ArgumentError) 2087 end 2088 end 2089 2090 context 'with a non-existing source commit' do 2091 let(:source_sha) { 'f001' } 2092 2093 it 'throws an ArgumentError' do 2094 expect { subject }.to raise_error(ArgumentError) 2095 end 2096 end 2097 2098 context 'when the source sha is not a descendant of the branch head' do 2099 let(:source_sha) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' } 2100 2101 it "doesn't perform the ff_merge" do 2102 expect { subject }.to raise_error(Gitlab::Git::CommitError) 2103 2104 expect(repository.commit(target_branch).id).to eq(branch_head) 2105 end 2106 end 2107 end 2108 2109 it "calls Gitaly's OperationService" do 2110 expect_any_instance_of(Gitlab::GitalyClient::OperationService) 2111 .to receive(:user_ff_branch).with(user, source_sha, target_branch) 2112 .and_return(nil) 2113 2114 subject 2115 end 2116 2117 it_behaves_like '#ff_merge' 2118 end 2119 2120 describe '#delete_all_refs_except' do 2121 let(:repository) { mutable_repository } 2122 2123 before do 2124 repository.write_ref("refs/delete/a", "0b4bc9a49b562e85de7cc9e834518ea6828729b9") 2125 repository.write_ref("refs/also-delete/b", "12d65c8dd2b2676fa3ac47d955accc085a37a9c1") 2126 repository.write_ref("refs/keep/c", "6473c90867124755509e100d0d35ebdc85a0b6ae") 2127 repository.write_ref("refs/also-keep/d", "0b4bc9a49b562e85de7cc9e834518ea6828729b9") 2128 end 2129 2130 after do 2131 ensure_seeds 2132 end 2133 2134 it 'deletes all refs except those with the specified prefixes' do 2135 repository.delete_all_refs_except(%w(refs/keep refs/also-keep refs/heads)) 2136 expect(repository.ref_exists?("refs/delete/a")).to be(false) 2137 expect(repository.ref_exists?("refs/also-delete/b")).to be(false) 2138 expect(repository.ref_exists?("refs/keep/c")).to be(true) 2139 expect(repository.ref_exists?("refs/also-keep/d")).to be(true) 2140 expect(repository.ref_exists?("refs/heads/master")).to be(true) 2141 end 2142 end 2143 2144 describe '#bundle_to_disk' do 2145 let(:save_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") } 2146 2147 after do 2148 FileUtils.rm_rf(save_path) 2149 end 2150 2151 it 'saves a bundle to disk' do 2152 repository.bundle_to_disk(save_path) 2153 2154 success = system( 2155 *%W(#{Gitlab.config.git.bin_path} -C #{repository_path} bundle verify #{save_path}), 2156 [:out, :err] => '/dev/null' 2157 ) 2158 expect(success).to be true 2159 end 2160 end 2161 2162 describe '#create_from_bundle' do 2163 let(:valid_bundle_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") } 2164 let(:malicious_bundle_path) { Rails.root.join('spec/fixtures/malicious.bundle') } 2165 let(:project) { create(:project) } 2166 let(:imported_repo) { project.repository.raw } 2167 2168 before do 2169 expect(repository.bundle_to_disk(valid_bundle_path)).to be_truthy 2170 end 2171 2172 after do 2173 FileUtils.rm_rf(valid_bundle_path) 2174 end 2175 2176 it 'creates a repo from a bundle file' do 2177 expect(imported_repo).not_to exist 2178 2179 result = imported_repo.create_from_bundle(valid_bundle_path) 2180 2181 expect(result).to be_truthy 2182 expect(imported_repo).to exist 2183 expect { imported_repo.fsck }.not_to raise_exception 2184 end 2185 2186 it 'raises an error if the bundle is an attempted malicious payload' do 2187 expect do 2188 imported_repo.create_from_bundle(malicious_bundle_path) 2189 end.to raise_error(::Gitlab::Git::BundleFile::InvalidBundleError) 2190 end 2191 end 2192 2193 describe '#compare_source_branch' do 2194 it 'delegates to Gitlab::Git::CrossRepoComparer' do 2195 expect_next_instance_of(::Gitlab::Git::CrossRepoComparer) do |instance| 2196 expect(instance.source_repo).to eq(:source_repository) 2197 expect(instance.target_repo).to eq(repository) 2198 2199 expect(instance).to receive(:compare).with('feature', 'master', straight: :straight) 2200 end 2201 2202 repository.compare_source_branch('master', :source_repository, 'feature', straight: :straight) 2203 end 2204 end 2205 2206 describe '#checksum' do 2207 it 'calculates the checksum for non-empty repo' do 2208 expect(repository.checksum).to eq '51d0a9662681f93e1fee547a6b7ba2bcaf716059' 2209 end 2210 2211 it 'returns 0000000000000000000000000000000000000000 for an empty repo' do 2212 FileUtils.rm_rf(File.join(storage_path, 'empty-repo.git')) 2213 2214 system(git_env, *%W(#{Gitlab.config.git.bin_path} init --bare empty-repo.git), 2215 chdir: storage_path, 2216 out: '/dev/null', 2217 err: '/dev/null') 2218 2219 empty_repo = described_class.new('default', 'empty-repo.git', '', 'group/empty-repo') 2220 2221 expect(empty_repo.checksum).to eq '0000000000000000000000000000000000000000' 2222 end 2223 2224 it 'raises Gitlab::Git::Repository::InvalidRepository error for non-valid git repo' do 2225 FileUtils.rm_rf(File.join(storage_path, 'non-valid.git')) 2226 2227 system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} non-valid.git), 2228 chdir: SEED_STORAGE_PATH, 2229 out: '/dev/null', 2230 err: '/dev/null') 2231 2232 File.truncate(File.join(storage_path, 'non-valid.git/HEAD'), 0) 2233 2234 non_valid = described_class.new('default', 'non-valid.git', '', 'a/non-valid') 2235 2236 expect { non_valid.checksum }.to raise_error(Gitlab::Git::Repository::InvalidRepository) 2237 end 2238 2239 it 'raises Gitlab::Git::Repository::NoRepository error when there is no repo' do 2240 broken_repo = described_class.new('default', 'a/path.git', '', 'a/path') 2241 2242 expect { broken_repo.checksum }.to raise_error(Gitlab::Git::Repository::NoRepository) 2243 end 2244 end 2245 2246 describe '#replicas', :praefect do 2247 it 'gets the replica checksum through praefect' do 2248 resp = repository.replicas 2249 2250 expect(resp.replicas).to be_empty 2251 expect(resp.primary.checksum).to eq(repository.checksum) 2252 end 2253 end 2254 2255 describe '#clean_stale_repository_files' do 2256 let(:worktree_id) { 'rebase-1' } 2257 let(:gitlab_worktree_path) { File.join(repository_path, 'gitlab-worktree', worktree_id) } 2258 let(:admin_dir) { File.join(repository_path, 'worktrees') } 2259 2260 it 'cleans up the files' do 2261 create_worktree = %W[git -C #{repository_path} worktree add --detach #{gitlab_worktree_path} master] 2262 raise 'preparation failed' unless system(*create_worktree, err: '/dev/null') 2263 2264 FileUtils.touch(gitlab_worktree_path, mtime: Time.now - 8.hours) 2265 # git rev-list --all will fail in git 2.16 if HEAD is pointing to a non-existent object, 2266 # but the HEAD must be 40 characters long or git will ignore it. 2267 File.write(File.join(admin_dir, worktree_id, 'HEAD'), Gitlab::Git::BLANK_SHA) 2268 2269 expect(rev_list_all).to be(false) 2270 repository.clean_stale_repository_files 2271 2272 expect(rev_list_all).to be(true) 2273 expect(File.exist?(gitlab_worktree_path)).to be_falsey 2274 end 2275 2276 def rev_list_all 2277 system(*%W[git -C #{repository_path} rev-list --all], out: '/dev/null', err: '/dev/null') 2278 end 2279 2280 it 'increments a counter upon an error' do 2281 expect(repository.gitaly_repository_client).to receive(:cleanup).and_raise(Gitlab::Git::CommandError) 2282 2283 counter = double(:counter) 2284 2285 expect(counter).to receive(:increment) 2286 expect(Gitlab::Metrics).to receive(:counter).with(:failed_repository_cleanup_total, 2287 'Number of failed repository cleanup events').and_return(counter) 2288 2289 repository.clean_stale_repository_files 2290 end 2291 end 2292 2293 describe '#squash' do 2294 let(:branch_name) { 'fix' } 2295 let(:start_sha) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' } 2296 let(:end_sha) { '12d65c8dd2b2676fa3ac47d955accc085a37a9c1' } 2297 2298 subject do 2299 opts = { 2300 branch: branch_name, 2301 start_sha: start_sha, 2302 end_sha: end_sha, 2303 author: user, 2304 message: 'Squash commit message' 2305 } 2306 2307 repository.squash(user, opts) 2308 end 2309 2310 # Should be ported to gitaly-ruby rspec suite https://gitlab.com/gitlab-org/gitaly/issues/1234 2311 skip 'sparse checkout' do 2312 let(:expected_files) { %w(files files/js files/js/application.js) } 2313 2314 it 'checks out only the files in the diff' do 2315 allow(repository).to receive(:with_worktree).and_wrap_original do |m, *args| 2316 m.call(*args) do 2317 worktree_path = args[0] 2318 files_pattern = File.join(worktree_path, '**', '*') 2319 expected = expected_files.map do |path| 2320 File.expand_path(path, worktree_path) 2321 end 2322 2323 expect(Dir[files_pattern]).to eq(expected) 2324 end 2325 end 2326 2327 subject 2328 end 2329 2330 context 'when the diff contains a rename' do 2331 let(:end_sha) { new_commit_move_file(repository_rugged).oid } 2332 2333 after do 2334 # Erase our commits so other tests get the original repo 2335 repository_rugged.references.update('refs/heads/master', SeedRepo::LastCommit::ID) 2336 end 2337 2338 it 'does not include the renamed file in the sparse checkout' do 2339 allow(repository).to receive(:with_worktree).and_wrap_original do |m, *args| 2340 m.call(*args) do 2341 worktree_path = args[0] 2342 files_pattern = File.join(worktree_path, '**', '*') 2343 2344 expect(Dir[files_pattern]).not_to include('CHANGELOG') 2345 expect(Dir[files_pattern]).not_to include('encoding/CHANGELOG') 2346 end 2347 end 2348 2349 subject 2350 end 2351 end 2352 end 2353 2354 # Should be ported to gitaly-ruby rspec suite https://gitlab.com/gitlab-org/gitaly/issues/1234 2355 skip 'with an ASCII-8BIT diff' do 2356 let(:diff) { "diff --git a/README.md b/README.md\nindex faaf198..43c5edf 100644\n--- a/README.md\n+++ b/README.md\n@@ -1,4 +1,4 @@\n-testme\n+✓ testme\n ======\n \n Sample repo for testing gitlab features\n" } 2357 2358 it 'applies a ASCII-8BIT diff' do 2359 allow(repository).to receive(:run_git!).and_call_original 2360 allow(repository).to receive(:run_git!).with(%W(diff --binary #{start_sha}...#{end_sha})).and_return(diff.force_encoding('ASCII-8BIT')) 2361 2362 expect(subject).to match(/\h{40}/) 2363 end 2364 end 2365 2366 # Should be ported to gitaly-ruby rspec suite https://gitlab.com/gitlab-org/gitaly/issues/1234 2367 skip 'with trailing whitespace in an invalid patch' do 2368 let(:diff) { "diff --git a/README.md b/README.md\nindex faaf198..43c5edf 100644\n--- a/README.md\n+++ b/README.md\n@@ -1,4 +1,4 @@\n-testme\n+ \n ====== \n \n Sample repo for testing gitlab features\n" } 2369 2370 it 'does not include whitespace warnings in the error' do 2371 allow(repository).to receive(:run_git!).and_call_original 2372 allow(repository).to receive(:run_git!).with(%W(diff --binary #{start_sha}...#{end_sha})).and_return(diff.force_encoding('ASCII-8BIT')) 2373 2374 expect { subject }.to raise_error do |error| 2375 expect(error).to be_a(described_class::GitError) 2376 expect(error.message).not_to include('trailing whitespace') 2377 end 2378 end 2379 end 2380 end 2381 2382 def create_remote_branch(remote_name, branch_name, source_branch_name) 2383 source_branch = repository.branches.find { |branch| branch.name == source_branch_name } 2384 repository_rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_branch.dereferenced_target.sha) 2385 end 2386 2387 def refs(dir) 2388 IO.popen(%W[git -C #{dir} for-each-ref], &:read).split("\n").map do |line| 2389 line.split("\t").last 2390 end 2391 end 2392 2393 describe '#disconnect_alternates' do 2394 let(:project) { create(:project, :repository) } 2395 let(:pool_repository) { create(:pool_repository) } 2396 let(:repository) { project.repository } 2397 let(:repository_path) { File.join(TestEnv.repos_path, repository.relative_path) } 2398 let(:object_pool) { pool_repository.object_pool } 2399 let(:object_pool_path) { File.join(TestEnv.repos_path, object_pool.repository.relative_path) } 2400 let(:object_pool_rugged) { Rugged::Repository.new(object_pool_path) } 2401 2402 before do 2403 object_pool.create # rubocop:disable Rails/SaveBang 2404 end 2405 2406 it 'does not raise an error when disconnecting a non-linked repository' do 2407 expect { repository.disconnect_alternates }.not_to raise_error 2408 end 2409 2410 it 'removes the alternates file' do 2411 object_pool.link(repository) 2412 2413 alternates_file = File.join(repository_path, "objects", "info", "alternates") 2414 expect(File.exist?(alternates_file)).to be_truthy 2415 2416 repository.disconnect_alternates 2417 2418 expect(File.exist?(alternates_file)).to be_falsey 2419 end 2420 2421 it 'can still access objects in the object pool' do 2422 object_pool.link(repository) 2423 new_commit = new_commit_edit_old_file(object_pool_rugged) 2424 expect(repository.commit(new_commit.oid).id).to eq(new_commit.oid) 2425 2426 repository.disconnect_alternates 2427 2428 expect(repository.commit(new_commit.oid).id).to eq(new_commit.oid) 2429 end 2430 end 2431 2432 describe '#rename' do 2433 let(:project) { create(:project, :repository)} 2434 let(:repository) { project.repository } 2435 2436 it 'moves the repository' do 2437 checksum = repository.checksum 2438 new_relative_path = "rename_test/relative/path" 2439 renamed_repository = Gitlab::Git::Repository.new(repository.storage, new_relative_path, nil, nil) 2440 2441 repository.rename(new_relative_path) 2442 2443 expect(renamed_repository.checksum).to eq(checksum) 2444 expect(repository.exists?).to be false 2445 end 2446 end 2447 2448 describe '#remove' do 2449 let(:project) { create(:project, :repository) } 2450 let(:repository) { project.repository } 2451 2452 it 'removes the repository' do 2453 expect(repository.exists?).to be true 2454 2455 repository.remove 2456 2457 expect(repository.raw_repository.exists?).to be false 2458 end 2459 2460 context 'when the repository does not exist' do 2461 let(:repository) { create(:project).repository } 2462 2463 it 'is idempotent' do 2464 expect(repository.exists?).to be false 2465 2466 repository.remove 2467 2468 expect(repository.raw_repository.exists?).to be false 2469 end 2470 end 2471 end 2472 2473 describe '#import_repository' do 2474 let_it_be(:project) { create(:project) } 2475 2476 let(:repository) { project.repository } 2477 let(:url) { 'http://invalid.invalid' } 2478 2479 it 'raises an error if a relative path is provided' do 2480 expect { repository.import_repository('/foo') }.to raise_error(ArgumentError, /disk path/) 2481 end 2482 2483 it 'raises an error if an absolute path is provided' do 2484 expect { repository.import_repository('./foo') }.to raise_error(ArgumentError, /disk path/) 2485 end 2486 2487 it 'delegates to Gitaly' do 2488 expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |svc| 2489 expect(svc).to receive(:import_repository).with(url).and_return(nil) 2490 end 2491 2492 repository.import_repository(url) 2493 end 2494 2495 it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RepositoryService, :import_repository do 2496 subject { repository.import_repository('http://invalid.invalid') } 2497 end 2498 end 2499 2500 describe '#replicate' do 2501 let(:new_repository) do 2502 Gitlab::Git::Repository.new('test_second_storage', TEST_REPO_PATH, '', 'group/project') 2503 end 2504 2505 let(:new_repository_path) { File.join(TestEnv::SECOND_STORAGE_PATH, new_repository.relative_path) } 2506 2507 subject { new_repository.replicate(repository) } 2508 2509 before do 2510 stub_storage_settings('test_second_storage' => { 2511 'gitaly_address' => Gitlab.config.repositories.storages.default.gitaly_address, 2512 'path' => TestEnv::SECOND_STORAGE_PATH 2513 }) 2514 end 2515 2516 after do 2517 new_repository.remove 2518 end 2519 2520 context 'destination does not exist' do 2521 it 'mirrors the source repository' do 2522 subject 2523 2524 expect(refs(new_repository_path)).to eq(refs(repository_path)) 2525 end 2526 end 2527 2528 context 'destination exists' do 2529 before do 2530 new_repository.create_repository 2531 end 2532 2533 it 'mirrors the source repository' do 2534 subject 2535 2536 expect(refs(new_repository_path)).to eq(refs(repository_path)) 2537 end 2538 2539 context 'with keep-around refs' do 2540 let(:sha) { SeedRepo::Commit::ID } 2541 let(:keep_around_ref) { "refs/keep-around/#{sha}" } 2542 let(:tmp_ref) { "refs/tmp/#{SecureRandom.hex}" } 2543 2544 before do 2545 repository.write_ref(keep_around_ref, sha) 2546 repository.write_ref(tmp_ref, sha) 2547 end 2548 2549 it 'includes the temporary and keep-around refs' do 2550 subject 2551 2552 expect(refs(new_repository_path)).to include(keep_around_ref) 2553 expect(refs(new_repository_path)).to include(tmp_ref) 2554 end 2555 end 2556 end 2557 end 2558end 2559