1# frozen_string_literal: true
2
3require 'fileutils'
4
5RSpec.shared_examples 'can collect git garbage' do |update_statistics: true|
6  include GitHelpers
7
8  let!(:lease_uuid) { SecureRandom.uuid }
9  let!(:lease_key) { "resource_housekeeping:#{resource.id}" }
10  let(:params) { [resource.id, task, lease_key, lease_uuid] }
11  let(:shell) { Gitlab::Shell.new }
12  let(:repository) { resource.repository }
13  let(:statistics_service_klass) { nil }
14
15  subject { described_class.new }
16
17  before do
18    allow(subject).to receive(:find_resource).and_return(resource)
19  end
20
21  shared_examples 'it calls Gitaly' do
22    specify do
23      repository_service = instance_double(Gitlab::GitalyClient::RepositoryService)
24
25      expect(subject).to receive(:get_gitaly_client).with(task, repository.raw_repository).and_return(repository_service)
26      expect(repository_service).to receive(gitaly_task)
27
28      subject.perform(*params)
29    end
30  end
31
32  shared_examples 'it updates the resource statistics' do
33    it 'updates the resource statistics' do
34      expect_next_instance_of(statistics_service_klass, anything, nil, statistics: statistics_keys) do |service|
35        expect(service).to receive(:execute)
36      end
37
38      subject.perform(*params)
39    end
40
41    it 'does nothing if the database is read-only' do
42      allow(Gitlab::Database).to receive(:read_only?) { true }
43
44      expect(statistics_service_klass).not_to receive(:new)
45
46      subject.perform(*params)
47    end
48  end
49
50  describe '#perform', :aggregate_failures do
51    let(:gitaly_task) { :garbage_collect }
52    let(:task) { :gc }
53
54    context 'with active lease_uuid' do
55      before do
56        allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
57      end
58
59      it_behaves_like 'it calls Gitaly'
60      it_behaves_like 'it updates the resource statistics' if update_statistics
61
62      it "flushes ref caches when the task if 'gc'" do
63        expect(subject).to receive(:renew_lease).with(lease_key, lease_uuid).and_call_original
64        expect(repository).to receive(:expire_branches_cache).and_call_original
65        expect(repository).to receive(:branch_names).and_call_original
66        expect(repository).to receive(:has_visible_content?).and_call_original
67        expect(repository.raw_repository).to receive(:has_visible_content?).and_call_original
68
69        subject.perform(*params)
70      end
71
72      it 'handles gRPC errors' do
73        allow_next_instance_of(Gitlab::GitalyClient::RepositoryService, repository.raw_repository) do |instance|
74          allow(instance).to receive(:garbage_collect).and_raise(GRPC::NotFound)
75        end
76
77        expect { subject.perform(*params) }.to raise_exception(Gitlab::Git::Repository::NoRepository)
78      end
79    end
80
81    context 'with different lease than the active one' do
82      before do
83        allow(subject).to receive(:get_lease_uuid).and_return(SecureRandom.uuid)
84      end
85
86      it 'returns silently' do
87        expect(repository).not_to receive(:expire_branches_cache).and_call_original
88        expect(repository).not_to receive(:branch_names).and_call_original
89        expect(repository).not_to receive(:has_visible_content?).and_call_original
90
91        subject.perform(*params)
92      end
93    end
94
95    context 'with no active lease' do
96      let(:params) { [resource.id] }
97
98      before do
99        allow(subject).to receive(:get_lease_uuid).and_return(false)
100      end
101
102      context 'when is able to get the lease' do
103        before do
104          allow(subject).to receive(:try_obtain_lease).and_return(SecureRandom.uuid)
105        end
106
107        it_behaves_like 'it calls Gitaly'
108        it_behaves_like 'it updates the resource statistics' if update_statistics
109
110        it "flushes ref caches when the task if 'gc'" do
111          expect(subject).to receive(:get_lease_uuid).with("git_gc:#{task}:#{expected_default_lease}").and_return(false)
112          expect(repository).to receive(:expire_branches_cache).and_call_original
113          expect(repository).to receive(:branch_names).and_call_original
114          expect(repository).to receive(:has_visible_content?).and_call_original
115          expect(repository.raw_repository).to receive(:has_visible_content?).and_call_original
116
117          subject.perform(*params)
118        end
119      end
120
121      context 'when no lease can be obtained' do
122        it 'returns silently' do
123          expect(subject).to receive(:try_obtain_lease).and_return(false)
124
125          expect(subject).not_to receive(:command)
126          expect(repository).not_to receive(:expire_branches_cache).and_call_original
127          expect(repository).not_to receive(:branch_names).and_call_original
128          expect(repository).not_to receive(:has_visible_content?).and_call_original
129
130          subject.perform(*params)
131        end
132      end
133    end
134
135    context 'repack_full' do
136      let(:task) { :full_repack }
137      let(:gitaly_task) { :repack_full }
138
139      before do
140        expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
141      end
142
143      it_behaves_like 'it calls Gitaly'
144      it_behaves_like 'it updates the resource statistics' if update_statistics
145    end
146
147    context 'pack_refs' do
148      let(:task) { :pack_refs }
149      let(:gitaly_task) { :pack_refs }
150
151      before do
152        expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
153      end
154
155      it 'calls Gitaly' do
156        repository_service = instance_double(Gitlab::GitalyClient::RefService)
157
158        expect(subject).to receive(:get_gitaly_client).with(task, repository.raw_repository).and_return(repository_service)
159        expect(repository_service).to receive(gitaly_task)
160
161        subject.perform(*params)
162      end
163
164      it 'does not update the resource statistics' do
165        expect(statistics_service_klass).not_to receive(:new)
166
167        subject.perform(*params)
168      end
169    end
170
171    context 'repack_incremental' do
172      let(:task) { :incremental_repack }
173      let(:gitaly_task) { :repack_incremental }
174
175      before do
176        expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
177      end
178
179      it_behaves_like 'it calls Gitaly'
180      it_behaves_like 'it updates the resource statistics' if update_statistics
181    end
182
183    shared_examples 'gc tasks' do
184      before do
185        allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
186        allow(subject).to receive(:bitmaps_enabled?).and_return(bitmaps_enabled)
187      end
188
189      it 'incremental repack adds a new packfile' do
190        create_objects(resource)
191        before_packs = packs(resource)
192
193        expect(before_packs.count).to be >= 1
194
195        subject.perform(resource.id, 'incremental_repack', lease_key, lease_uuid)
196        after_packs = packs(resource)
197
198        # Exactly one new pack should have been created
199        expect(after_packs.count).to eq(before_packs.count + 1)
200
201        # Previously existing packs are still around
202        expect(before_packs & after_packs).to eq(before_packs)
203      end
204
205      it 'full repack consolidates into 1 packfile' do
206        create_objects(resource)
207        subject.perform(resource.id, 'incremental_repack', lease_key, lease_uuid)
208        before_packs = packs(resource)
209
210        expect(before_packs.count).to be >= 2
211
212        subject.perform(resource.id, 'full_repack', lease_key, lease_uuid)
213        after_packs = packs(resource)
214
215        expect(after_packs.count).to eq(1)
216
217        # Previously existing packs should be gone now
218        expect(after_packs - before_packs).to eq(after_packs)
219
220        expect(File.exist?(bitmap_path(after_packs.first))).to eq(bitmaps_enabled)
221      end
222
223      it 'gc consolidates into 1 packfile and updates packed-refs' do
224        create_objects(resource)
225        before_packs = packs(resource)
226        before_packed_refs = packed_refs(resource)
227
228        expect(before_packs.count).to be >= 1
229
230        # It's quite difficult to use `expect_next_instance_of` in this place
231        # because the RepositoryService is instantiated several times to do
232        # some repository calls like `exists?`, `create_repository`, ... .
233        # Therefore, since we're instantiating the object several times,
234        # RSpec has troubles figuring out which instance is the next and which
235        # one we want to mock.
236        # Besides, at this point, we actually want to perform the call to Gitaly,
237        # otherwise we would just use `instance_double` like in other parts of the
238        # spec file.
239        expect_any_instance_of(Gitlab::GitalyClient::RepositoryService) # rubocop:disable RSpec/AnyInstanceOf
240          .to receive(:garbage_collect)
241          .with(bitmaps_enabled, prune: false)
242          .and_call_original
243
244        subject.perform(resource.id, 'gc', lease_key, lease_uuid)
245        after_packed_refs = packed_refs(resource)
246        after_packs = packs(resource)
247
248        expect(after_packs.count).to eq(1)
249
250        # Previously existing packs should be gone now
251        expect(after_packs - before_packs).to eq(after_packs)
252
253        # The packed-refs file should have been updated during 'git gc'
254        expect(before_packed_refs).not_to eq(after_packed_refs)
255
256        expect(File.exist?(bitmap_path(after_packs.first))).to eq(bitmaps_enabled)
257      end
258
259      it 'cleans up repository after finishing' do
260        expect(resource).to receive(:cleanup).and_call_original
261
262        subject.perform(resource.id, 'gc', lease_key, lease_uuid)
263      end
264
265      it 'prune calls garbage_collect with the option prune: true' do
266        repository_service = instance_double(Gitlab::GitalyClient::RepositoryService)
267
268        expect(subject).to receive(:get_gitaly_client).with(:prune, repository.raw_repository).and_return(repository_service)
269        expect(repository_service).to receive(:garbage_collect).with(bitmaps_enabled, prune: true)
270
271        subject.perform(resource.id, 'prune', lease_key, lease_uuid)
272      end
273
274      # Create a new commit on a random new branch
275      def create_objects(resource)
276        rugged = rugged_repo(resource.repository)
277        old_commit = rugged.branches.first.target
278        new_commit_sha = Rugged::Commit.create(
279          rugged,
280          message: "hello world #{SecureRandom.hex(6)}",
281          author: { email: 'foo@bar', name: 'baz' },
282          committer: { email: 'foo@bar', name: 'baz' },
283          tree: old_commit.tree,
284          parents: [old_commit]
285        )
286        rugged.references.create("refs/heads/#{SecureRandom.hex(6)}", new_commit_sha)
287      end
288
289      def packs(resource)
290        Dir["#{path_to_repo}/objects/pack/*.pack"]
291      end
292
293      def packed_refs(resource)
294        path = File.join(path_to_repo, 'packed-refs')
295        FileUtils.touch(path)
296        File.read(path)
297      end
298
299      def path_to_repo
300        @path_to_repo ||= File.join(TestEnv.repos_path, resource.repository.relative_path)
301      end
302
303      def bitmap_path(pack)
304        pack.sub(/\.pack\z/, '.bitmap')
305      end
306    end
307
308    context 'with bitmaps enabled' do
309      let(:bitmaps_enabled) { true }
310
311      include_examples 'gc tasks'
312    end
313
314    context 'with bitmaps disabled' do
315      let(:bitmaps_enabled) { false }
316
317      include_examples 'gc tasks'
318    end
319  end
320end
321