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