repository.rb 12.8 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)
240
    last_commit = commit(branch_name)
241

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)
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
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

Stan Hu's avatar
Stan Hu committed
376
  def commit_dir(user, path, message, branch)
377
    commit_with_hooks(user, branch) do |ref|
Stan Hu's avatar
Stan Hu committed
378 379 380 381 382 383 384 385 386 387 388 389 390
      committer = user_to_committer(user)
      options = {}
      options[:committer] = committer
      options[:author] = committer

      options[:commit] = {
        message: message,
        branch: ref,
      }

      raw_repository.mkdir(path, options)
    end
  end
391

Stan Hu's avatar
Stan Hu committed
392 393 394
  def commit_file(user, path, content, message, branch, update)
    commit_with_hooks(user, branch) do |ref|
      committer = user_to_committer(user)
395 396 397 398 399 400 401
      options = {}
      options[:committer] = committer
      options[:author] = committer
      options[:commit] = {
        message: message,
        branch: ref,
      }
402

403 404
      options[:file] = {
        content: content,
Stan Hu's avatar
Stan Hu committed
405 406
        path: path,
        update: update
407
      }
408

409 410
      Gitlab::Git::Blob.commit(raw_repository, options)
    end
411 412
  end

413
  def remove_file(user, path, message, branch)
414
    commit_with_hooks(user, branch) do |ref|
Stan Hu's avatar
Stan Hu committed
415
      committer = user_to_committer(user)
416 417 418 419 420 421 422
      options = {}
      options[:committer] = committer
      options[:author] = committer
      options[:commit] = {
        message: message,
        branch: ref
      }
423

424 425 426
      options[:file] = {
        path: path
      }
427

428 429
      Gitlab::Git::Blob.remove(raw_repository, options)
    end
430 431
  end

Stan Hu's avatar
Stan Hu committed
432
  def user_to_committer(user)
433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450
    {
      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

451
  def merge(user, source_sha, target_branch, options = {})
452 453 454 455 456 457 458 459 460
    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?

461 462 463 464 465 466
    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
      )
467

468 469
      Rugged::Commit.create(rugged, actual_options)
    end
470 471
  end

472 473 474 475 476 477 478 479 480 481 482
  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

483 484
  def search_files(query, ref)
    offset = 2
485
    args = %W(git grep -i -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref})
486 487 488
    Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
  end

489
  def parse_search_result(result)
490 491 492 493
    ref = nil
    filename = nil
    startline = 0

494
    result.each_line.each_with_index do |line, index|
495 496 497 498 499 500 501
      if line =~ /^.*:.*:\d+:/
        ref, filename, startline = line.split(':')
        startline = startline.to_i - index
        break
      end
    end

502
    data = ""
503

504 505 506
    result.each_line do |line|
      data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '')
    end
507 508 509 510 511 512 513 514 515

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

516 517 518 519 520
  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

521 522 523 524
  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)
525
    was_empty = empty?
526 527 528 529 530

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

531
    unless was_empty
532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547
      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
548
      if was_empty
549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564
        # 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)
565
      post_receive_hook.trigger(gl_id, oldrev, newrev, ref)
566 567 568 569
    else
      # Remove tmp ref and return error to user
      rugged.references.delete(tmp_ref)

570
      raise PreReceiveError.new('Commit was rejected by pre-receive hook')
571 572 573
    end
  end

574 575
  private

576 577 578
  def cache
    @cache ||= RepositoryCache.new(path_with_namespace)
  end
579
end