repository.rb 12.5 KB
Newer Older
1
2
require 'securerandom'

3
class Repository
4
5
6
  class PreReceiveError < StandardError; end
  class CommitError < StandardError; end

7
8
  include Gitlab::ShellAdapter

9
  attr_accessor :raw_repository, :path_with_namespace, :project
10

11
  def initialize(path_with_namespace, default_branch = nil, project = nil)
12
    @path_with_namespace = path_with_namespace
13
    @project = project
14
15

    if path_with_namespace
16
      @raw_repository = Gitlab::Git::Repository.new(path_to_repo)
17
18
19
      @raw_repository.autocrlf = :input
    end

20
21
22
23
  rescue Gitlab::Git::Repository::NoRepository
    nil
  end

24
  # Return absolute path to repository
25
  def path_to_repo
26
27
28
    @path_to_repo ||= File.expand_path(
      File.join(Gitlab.config.gitlab_shell.repos_path, path_with_namespace + ".git")
    )
29
30
  end

31
32
33
34
35
36
  def exists?
    raw_repository
  end

  def empty?
    raw_repository.empty?
37
38
  end

39
  def commit(id = 'HEAD')
40
    return nil unless raw_repository
41
    commit = Gitlab::Git::Commit.find(raw_repository, id)
42
    commit = Commit.new(commit, @project) if commit
43
    commit
44
  rescue Rugged::OdbError
45
    nil
46
47
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
48
  def commits(ref, path = nil, limit = nil, offset = nil, skip_merges = false)
49
50
51
52
53
54
55
    commits = Gitlab::Git::Commit.where(
      repo: raw_repository,
      ref: ref,
      path: path,
      limit: limit,
      offset: offset,
    )
56
    commits = Commit.decorate(commits, @project) if commits.present?
57
58
59
    commits
  end

60
61
  def commits_between(from, to)
    commits = Gitlab::Git::Commit.between(raw_repository, from, to)
62
    commits = Commit.decorate(commits, @project) if commits.present?
63
64
65
    commits
  end

66
67
68
69
70
71
72
73
  def find_branch(name)
    branches.find { |branch| branch.name == name }
  end

  def find_tag(name)
    tags.find { |tag| tag.name == name }
  end

74
  def add_branch(branch_name, ref)
75
    cache.expire(:branch_names)
76
    @branches = nil
77
78
79
80

    gitlab_shell.add_branch(path_with_namespace, branch_name, ref)
  end

81
  def add_tag(tag_name, ref, message = nil)
82
    cache.expire(:tag_names)
83
    @tags = nil
84

85
    gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message)
86
87
  end

88
  def rm_branch(branch_name)
89
    cache.expire(:branch_names)
90
    @branches = nil
91

92
93
94
    gitlab_shell.rm_branch(path_with_namespace, branch_name)
  end

95
  def rm_tag(tag_name)
96
    cache.expire(:tag_names)
97
    @tags = nil
98

99
100
101
    gitlab_shell.rm_tag(path_with_namespace, tag_name)
  end

102
  def branch_names
103
    cache.fetch(:branch_names) { raw_repository.branch_names }
104
105
106
  end

  def tag_names
107
    cache.fetch(:tag_names) { raw_repository.tag_names }
108
109
  end

110
  def commit_count
111
    cache.fetch(:commit_count) do
112
      begin
113
        raw_repository.commit_count(self.root_ref)
114
115
116
      rescue
        0
      end
117
    end
118
119
  end

120
121
122
  # Return repo size in megabytes
  # Cached in redis
  def size
123
    cache.fetch(:size) { raw_repository.size }
124
125
  end

126
  def cache_keys
127
    %i(size branch_names tag_names commit_count
128
129
130
131
132
133
134
135
136
137
138
       readme version contribution_guide changelog license)
  end

  def build_cache
    cache_keys.each do |key|
      unless cache.exist?(key)
        send(key)
      end
    end
  end

139
  def expire_cache
140
    cache_keys.each do |key|
141
142
      cache.expire(key)
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
143
144
  end

145
146
  def rebuild_cache
    cache_keys.each do |key|
147
      cache.expire(key)
148
      send(key)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
149
    end
150
151
  end

152
153
154
155
  def lookup_cache
    @lookup_cache ||= {}
  end

156
157
158
159
  def expire_branch_names
    cache.expire(:branch_names)
  end

160
  def method_missing(m, *args, &block)
161
162
163
164
165
166
    if m == :lookup && !block_given?
      lookup_cache[m] ||= {}
      lookup_cache[m][args.join(":")] ||= raw_repository.send(m, *args, &block)
    else
      raw_repository.send(m, *args, &block)
    end
167
168
  end

169
170
  def respond_to_missing?(method, include_private = false)
    raw_repository.respond_to?(method, include_private) || super
171
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
172
173

  def blob_at(sha, path)
174
175
176
    unless Gitlab::Git.blank_ref?(sha)
      Gitlab::Git::Blob.find(self, sha, path)
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
177
  end
178

179
180
181
182
  def blob_by_oid(oid)
    Gitlab::Git::Blob.raw(self, oid)
  end

183
  def readme
184
    cache.fetch(:readme) { tree(:head).readme }
185
  end
186

187
  def version
188
    cache.fetch(:version) do
189
190
191
192
193
194
      tree(:head).blobs.find do |file|
        file.name.downcase == 'version'
      end
    end
  end

195
  def contribution_guide
196
197
198
199
200
201
    cache.fetch(:contribution_guide) do
      tree(:head).blobs.find do |file|
        file.contributing?
      end
    end
  end
202
203
204
205

  def changelog
    cache.fetch(:changelog) do
      tree(:head).blobs.find do |file|
206
        file.name =~ /\A(changelog|history)/i
207
208
      end
    end
209
210
  end

211
212
213
  def license
    cache.fetch(:license) do
      tree(:head).blobs.find do |file|
214
        file.name =~ /\Alicense/i
215
216
      end
    end
217
218
  end

219
  def head_commit
220
221
222
223
224
    @head_commit ||= commit(self.root_ref)
  end

  def head_tree
    @head_tree ||= Tree.new(self, head_commit.sha, nil)
225
226
227
228
  end

  def tree(sha = :head, path = nil)
    if sha == :head
229
230
231
232
233
      if path.nil?
        return head_tree
      else
        sha = head_commit.sha
      end
234
235
236
237
    end

    Tree.new(self, sha, path)
  end
238
239

  def blob_at_branch(branch_name, path)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
240
    last_commit = commit(branch_name)
241

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
242
243
244
245
246
    if last_commit
      blob_at(last_commit.sha, path)
    else
      nil
    end
247
  end
248
249
250
251
252
253
254
255

  # Returns url for submodule
  #
  # Ex.
  #   @repository.submodule_url_for('master', 'rack')
  #   # => git@localhost:rack.git
  #
  def submodule_url_for(ref, path)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
256
    if submodules(ref).any?
257
258
259
260
261
262
263
      submodule = submodules(ref)[path]

      if submodule
        submodule['url']
      end
    end
  end
264
265

  def last_commit_for_path(sha, path)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
266
    args = %W(git rev-list --max-count=1 #{sha} -- #{path})
267
268
    sha = Gitlab::Popen.popen(args, path_to_repo).first.strip
    commit(sha)
269
  end
270
271
272

  # Remove archives older than 2 hours
  def clean_old_archives
273
    repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path
274
275
276

    return unless File.directory?(repository_downloads_path)

277
    Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete))
278
  end
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293

  def branches_sorted_by(value)
    case value
    when 'recently_updated'
      branches.sort do |a, b|
        commit(b.target).committed_date <=> commit(a.target).committed_date
      end
    when 'last_updated'
      branches.sort do |a, b|
        commit(a.target).committed_date <=> commit(b.target).committed_date
      end
    else
      branches
    end
  end
294
295

  def contributors
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
296
    commits = self.commits(nil, nil, 2000, 0, true)
297

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
298
    commits.group_by(&:author_email).map do |email, commits|
299
300
      contributor = Gitlab::Contributor.new
      contributor.email = email
301

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
302
      commits.each do |commit|
303
        if contributor.name.blank?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
304
          contributor.name = commit.author_name
305
306
        end

307
        contributor.commits += 1
308
309
      end

310
311
      contributor
    end
312
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328

  def blob_for_diff(commit, diff)
    file = blob_at(commit.id, diff.new_path)

    unless file
      file = prev_blob_for_diff(commit, diff)
    end

    file
  end

  def prev_blob_for_diff(commit, diff)
    if commit.parent_id
      blob_at(commit.parent_id, diff.old_path)
    end
  end
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345

  def branch_names_contains(sha)
    args = %W(git branch --contains #{sha})
    names = Gitlab::Popen.popen(args, path_to_repo).first

    if names.respond_to?(:split)
      names = names.split("\n").map(&:strip)

      names.each do |name|
        name.slice! '* '
      end

      names
    else
      []
    end
  end
Hannes Rosenögger's avatar
Hannes Rosenögger committed
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362

  def tag_names_contains(sha)
    args = %W(git tag --contains #{sha})
    names = Gitlab::Popen.popen(args, path_to_repo).first

    if names.respond_to?(:split)
      names = names.split("\n").map(&:strip)

      names.each do |name|
        name.slice! '* '
      end

      names
    else
      []
    end
  end
363

364
365
366
367
368
369
370
371
372
373
374
375
  def branches
    @branches ||= raw_repository.branches
  end

  def tags
    @tags ||= raw_repository.tags
  end

  def root_ref
    @root_ref ||= raw_repository.root_ref
  end

376
377
378
  def commit_file(user, path, content, message, branch)
    commit_with_hooks(user, branch) do |ref|
      path[0] = '' if path[0] == '/'
379

380
381
382
383
384
385
386
387
      committer = user_to_comitter(user)
      options = {}
      options[:committer] = committer
      options[:author] = committer
      options[:commit] = {
        message: message,
        branch: ref,
      }
388

389
390
391
392
      options[:file] = {
        content: content,
        path: path
      }
393

394
395
      Gitlab::Git::Blob.commit(raw_repository, options)
    end
396
397
  end

398
  def remove_file(user, path, message, branch)
399
    commit_with_hooks(user, branch) do |ref|
400
      path[0] = '' if path[0] == '/'
401

402
403
404
405
406
407
408
409
      committer = user_to_comitter(user)
      options = {}
      options[:committer] = committer
      options[:author] = committer
      options[:commit] = {
        message: message,
        branch: ref
      }
410

411
412
413
      options[:file] = {
        path: path
      }
414

415
416
      Gitlab::Git::Blob.remove(raw_repository, options)
    end
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
  end

  def user_to_comitter(user)
    {
      email: user.email,
      name: user.name,
      time: Time.now
    }
  end

  def can_be_merged?(source_sha, target_branch)
    our_commit = rugged.branches[target_branch].target
    their_commit = rugged.lookup(source_sha)

    if our_commit && their_commit
      !rugged.merge_commits(our_commit, their_commit).conflicts?
    else
      false
    end
  end

438
  def merge(user, source_sha, target_branch, options = {})
439
440
441
442
443
444
445
446
447
    our_commit = rugged.branches[target_branch].target
    their_commit = rugged.lookup(source_sha)

    raise "Invalid merge target" if our_commit.nil?
    raise "Invalid merge source" if their_commit.nil?

    merge_index = rugged.merge_commits(our_commit, their_commit)
    return false if merge_index.conflicts?

448
449
450
451
452
453
    commit_with_hooks(user, target_branch) do |ref|
      actual_options = options.merge(
        parents: [our_commit, their_commit],
        tree: merge_index.write_tree(rugged),
        update_ref: ref
      )
454

455
456
      Rugged::Commit.create(rugged, actual_options)
    end
457
458
  end

459
460
461
462
463
464
465
466
467
468
469
  def merged_to_root_ref?(branch_name)
    branch_commit = commit(branch_name)
    root_ref_commit = commit(root_ref)

    if branch_commit
      rugged.merge_base(root_ref_commit.id, branch_commit.id) == branch_commit.id
    else
      nil
    end
  end

470
471
472
473
474
475
  def search_files(query, ref)
    offset = 2
    args = %W(git grep -i -n --before-context #{offset} --after-context #{offset} #{query} #{ref || root_ref})
    Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
  end

476
  def parse_search_result(result)
477
478
479
480
    ref = nil
    filename = nil
    startline = 0

481
    result.each_line.each_with_index do |line, index|
482
483
484
485
486
487
488
      if line =~ /^.*:.*:\d+:/
        ref, filename, startline = line.split(':')
        startline = startline.to_i - index
        break
      end
    end

489
    data = ""
490

491
492
493
    result.each_line do |line|
      data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '')
    end
494
495
496
497
498
499
500
501
502

    OpenStruct.new(
      filename: filename,
      ref: ref,
      startline: startline,
      data: data
    )
  end

503
504
505
506
507
  def fetch_ref(source_path, source_ref, target_ref)
    args = %W(git fetch #{source_path} #{source_ref}:#{target_ref})
    Gitlab::Popen.popen(args, path_to_repo)
  end

508
509
510
511
  def commit_with_hooks(current_user, branch)
    oldrev = Gitlab::Git::BLANK_SHA
    ref = Gitlab::Git::BRANCH_REF_PREFIX + branch
    gl_id = Gitlab::ShellEnv.gl_id(current_user)
512
    was_empty = empty?
513
514
515
516
517

    # Create temporary ref
    random_string = SecureRandom.hex
    tmp_ref = "refs/tmp/#{random_string}/head"

518
    unless was_empty
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
      oldrev = find_branch(branch).target
      rugged.references.create(tmp_ref, oldrev)
    end

    # Make commit in tmp ref
    newrev = yield(tmp_ref)

    unless newrev
      raise CommitError.new('Failed to create commit')
    end

    # Run GitLab pre-receive hook
    pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', path_to_repo)
    status = pre_receive_hook.trigger(gl_id, oldrev, newrev, ref)

    if status
535
      if was_empty
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
        # Create branch
        rugged.references.create(ref, newrev)
      else
        # Update head
        current_head = find_branch(branch).target

        # Make sure target branch was not changed during pre-receive hook
        if current_head == oldrev
          rugged.references.update(ref, newrev)
        else
          raise CommitError.new('Commit was rejected because branch received new push')
        end
      end

      # Run GitLab post receive hook
      post_receive_hook = Gitlab::Git::Hook.new('post-receive', path_to_repo)
552
      post_receive_hook.trigger(gl_id, oldrev, newrev, ref)
553
554
555
556
    else
      # Remove tmp ref and return error to user
      rugged.references.delete(tmp_ref)

557
      raise PreReceiveError.new('Commit was rejected by pre-receive hook')
558
559
560
    end
  end

561
562
  private

563
564
565
  def cache
    @cache ||= RepositoryCache.new(path_with_namespace)
  end
566
end