repository.rb 10.3 KB
Newer Older
1
class Repository
2 3
  include Gitlab::ShellAdapter

4
  attr_accessor :raw_repository, :path_with_namespace, :project
5

6
  def initialize(path_with_namespace, default_branch = nil, project = nil)
7
    @path_with_namespace = path_with_namespace
8
    @project = project
9 10

    if path_with_namespace
11
      @raw_repository = Gitlab::Git::Repository.new(path_to_repo)
12 13 14
      @raw_repository.autocrlf = :input
    end

15 16 17 18
  rescue Gitlab::Git::Repository::NoRepository
    nil
  end

19
  # Return absolute path to repository
20
  def path_to_repo
21 22 23
    @path_to_repo ||= File.expand_path(
      File.join(Gitlab.config.gitlab_shell.repos_path, path_with_namespace + ".git")
    )
24 25
  end

26 27 28 29 30 31
  def exists?
    raw_repository
  end

  def empty?
    raw_repository.empty?
32 33
  end

34
  def commit(id = 'HEAD')
35
    return nil unless raw_repository
36
    commit = Gitlab::Git::Commit.find(raw_repository, id)
37
    commit = Commit.new(commit, @project) if commit
38
    commit
39
  rescue Rugged::OdbError
40
    nil
41 42
  end

D
Dmitriy Zaporozhets 已提交
43
  def commits(ref, path = nil, limit = nil, offset = nil, skip_merges = false)
44 45 46 47 48 49 50
    commits = Gitlab::Git::Commit.where(
      repo: raw_repository,
      ref: ref,
      path: path,
      limit: limit,
      offset: offset,
    )
51
    commits = Commit.decorate(commits, @project) if commits.present?
52 53 54
    commits
  end

55 56
  def commits_between(from, to)
    commits = Gitlab::Git::Commit.between(raw_repository, from, to)
57
    commits = Commit.decorate(commits, @project) if commits.present?
58 59 60
    commits
  end

61 62 63 64 65 66 67 68
  def find_branch(name)
    branches.find { |branch| branch.name == name }
  end

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

69
  def add_branch(branch_name, ref)
70
    cache.expire(:branch_names)
71
    @branches = nil
72 73 74 75

    gitlab_shell.add_branch(path_with_namespace, branch_name, ref)
  end

76
  def add_tag(tag_name, ref, message = nil)
77
    cache.expire(:tag_names)
78
    @tags = nil
79

80
    gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message)
81 82
  end

83
  def rm_branch(branch_name)
84
    cache.expire(:branch_names)
85
    @branches = nil
86

87 88 89
    gitlab_shell.rm_branch(path_with_namespace, branch_name)
  end

90
  def rm_tag(tag_name)
91
    cache.expire(:tag_names)
92
    @tags = nil
93

94 95 96
    gitlab_shell.rm_tag(path_with_namespace, tag_name)
  end

97
  def branch_names
98
    cache.fetch(:branch_names) { raw_repository.branch_names }
99 100 101
  end

  def tag_names
102
    cache.fetch(:tag_names) { raw_repository.tag_names }
103 104
  end

105
  def commit_count
106
    cache.fetch(:commit_count) do
107
      begin
108
        raw_repository.commit_count(self.root_ref)
109 110 111
      rescue
        0
      end
112
    end
113 114
  end

115 116 117
  # Return repo size in megabytes
  # Cached in redis
  def size
118
    cache.fetch(:size) { raw_repository.size }
119 120
  end

121
  def cache_keys
122
    %i(size branch_names tag_names commit_count
123 124 125 126 127 128 129 130 131 132 133
       readme version contribution_guide changelog license)
  end

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

134
  def expire_cache
135
    cache_keys.each do |key|
136 137
      cache.expire(key)
    end
D
Dmitriy Zaporozhets 已提交
138 139
  end

140 141
  def rebuild_cache
    cache_keys.each do |key|
142
      cache.expire(key)
143
      send(key)
D
Dmitriy Zaporozhets 已提交
144
    end
145 146
  end

147 148 149 150
  def lookup_cache
    @lookup_cache ||= {}
  end

151
  def method_missing(m, *args, &block)
152 153 154 155 156 157
    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
158 159
  end

160 161
  def respond_to_missing?(method, include_private = false)
    raw_repository.respond_to?(method, include_private) || super
162
  end
D
Dmitriy Zaporozhets 已提交
163 164

  def blob_at(sha, path)
165 166 167
    unless Gitlab::Git.blank_ref?(sha)
      Gitlab::Git::Blob.find(self, sha, path)
    end
D
Dmitriy Zaporozhets 已提交
168
  end
169

170 171 172 173
  def blob_by_oid(oid)
    Gitlab::Git::Blob.raw(self, oid)
  end

174
  def readme
175
    cache.fetch(:readme) { tree(:head).readme }
176
  end
177

178
  def version
179
    cache.fetch(:version) do
180 181 182 183 184 185
      tree(:head).blobs.find do |file|
        file.name.downcase == 'version'
      end
    end
  end

186
  def contribution_guide
187 188 189 190 191 192
    cache.fetch(:contribution_guide) do
      tree(:head).blobs.find do |file|
        file.contributing?
      end
    end
  end
193 194 195 196

  def changelog
    cache.fetch(:changelog) do
      tree(:head).blobs.find do |file|
197
        file.name =~ /\A(changelog|history)/i
198 199
      end
    end
200 201
  end

202 203 204
  def license
    cache.fetch(:license) do
      tree(:head).blobs.find do |file|
205
        file.name =~ /\Alicense/i
206 207
      end
    end
208 209
  end

210
  def head_commit
211 212 213 214 215
    @head_commit ||= commit(self.root_ref)
  end

  def head_tree
    @head_tree ||= Tree.new(self, head_commit.sha, nil)
216 217 218 219
  end

  def tree(sha = :head, path = nil)
    if sha == :head
220 221 222 223 224
      if path.nil?
        return head_tree
      else
        sha = head_commit.sha
      end
225 226 227 228
    end

    Tree.new(self, sha, path)
  end
D
Dmitriy Zaporozhets 已提交
229 230

  def blob_at_branch(branch_name, path)
D
Dmitriy Zaporozhets 已提交
231
    last_commit = commit(branch_name)
D
Dmitriy Zaporozhets 已提交
232

D
Dmitriy Zaporozhets 已提交
233 234 235 236 237
    if last_commit
      blob_at(last_commit.sha, path)
    else
      nil
    end
D
Dmitriy Zaporozhets 已提交
238
  end
D
Dmitriy Zaporozhets 已提交
239 240 241 242 243 244 245 246

  # Returns url for submodule
  #
  # Ex.
  #   @repository.submodule_url_for('master', 'rack')
  #   # => git@localhost:rack.git
  #
  def submodule_url_for(ref, path)
D
Dmitriy Zaporozhets 已提交
247
    if submodules(ref).any?
D
Dmitriy Zaporozhets 已提交
248 249 250 251 252 253 254
      submodule = submodules(ref)[path]

      if submodule
        submodule['url']
      end
    end
  end
255 256

  def last_commit_for_path(sha, path)
D
Dmitriy Zaporozhets 已提交
257
    args = %W(git rev-list --max-count=1 #{sha} -- #{path})
258 259
    sha = Gitlab::Popen.popen(args, path_to_repo).first.strip
    commit(sha)
260
  end
261 262 263

  # Remove archives older than 2 hours
  def clean_old_archives
264
    repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path
265 266 267

    return unless File.directory?(repository_downloads_path)

268
    Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete))
269
  end
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284

  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
285 286

  def contributors
D
Dmitriy Zaporozhets 已提交
287
    commits = self.commits(nil, nil, 2000, 0, true)
288

D
Dmitriy Zaporozhets 已提交
289
    commits.group_by(&:author_email).map do |email, commits|
290 291
      contributor = Gitlab::Contributor.new
      contributor.email = email
292

D
Dmitriy Zaporozhets 已提交
293
      commits.each do |commit|
294
        if contributor.name.blank?
D
Dmitriy Zaporozhets 已提交
295
          contributor.name = commit.author_name
296 297
        end

298
        contributor.commits += 1
299 300
      end

301 302
      contributor
    end
303
  end
D
Dmitriy Zaporozhets 已提交
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319

  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
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336

  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
H
Hannes Rosenögger 已提交
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353

  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
354

355 356 357 358 359 360 361 362 363 364 365 366
  def branches
    @branches ||= raw_repository.branches
  end

  def tags
    @tags ||= raw_repository.tags
  end

  def root_ref
    @root_ref ||= raw_repository.root_ref
  end

367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443
  def commit_file(user, path, content, message, ref)
    path[0] = '' if path[0] == '/'

    committer = user_to_comitter(user)
    options = {}
    options[:committer] = committer
    options[:author] = committer
    options[:commit] = {
      message: message,
      branch: ref
    }

    options[:file] = {
      content: content,
      path: path
    }

    Gitlab::Git::Blob.commit(raw_repository, options)
  end

  def remove_file(user, path, message, ref)
    path[0] = '' if path[0] == '/'

    committer = user_to_comitter(user)
    options = {}
    options[:committer] = committer
    options[:author] = committer
    options[:commit] = {
      message: message,
      branch: ref
    }

    options[:file] = {
      path: path
    }

    Gitlab::Git::Blob.remove(raw_repository, options)
  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

  def merge(source_sha, target_branch, options = {})
    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?

    actual_options = options.merge(
      parents: [our_commit, their_commit],
      tree: merge_index.write_tree(rugged),
      update_ref: "refs/heads/#{target_branch}"
    )

    Rugged::Commit.create(rugged, actual_options)
  end

444 445 446 447 448 449
  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

D
Dmitriy Zaporozhets 已提交
450
  def parse_search_result(result)
451 452 453 454
    ref = nil
    filename = nil
    startline = 0

455
    result.each_line.each_with_index do |line, index|
456 457 458 459 460 461 462
      if line =~ /^.*:.*:\d+:/
        ref, filename, startline = line.split(':')
        startline = startline.to_i - index
        break
      end
    end

463
    data = ""
464

465 466 467
    result.each_line do |line|
      data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '')
    end
468 469 470 471 472 473 474 475 476

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

477 478 479 480 481
  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

482 483
  private

484 485 486
  def cache
    @cache ||= RepositoryCache.new(path_with_namespace)
  end
487
end