From 676bce2ab941fcc50ef5796ff77229e238eb9b35 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 31 Mar 2013 18:49:06 +0300 Subject: [PATCH] Move Commit and Repository logic to lib/gitlab/git --- lib/gitlab/git/commit.rb | 179 ++++++++++++++++++++++++++++++++++ lib/gitlab/git/repository.rb | 181 +++++++++++++++++++++++++++++++++++ 2 files changed, 360 insertions(+) create mode 100644 lib/gitlab/git/commit.rb create mode 100644 lib/gitlab/git/repository.rb diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb new file mode 100644 index 00000000..86e0dfbb --- /dev/null +++ b/lib/gitlab/git/commit.rb @@ -0,0 +1,179 @@ +# Gitlab::Git::Gitlab::Git::Commit is a wrapper around native Grit::Commit object +# We dont want to use grit objects inside app/ +# It helps us easily migrate to rugged in future +module Gitlab + module Git + class Gitlab::Git::Commit + attr_accessor :raw_commit, :head, :refs + + delegate :message, :authored_date, :committed_date, :parents, :sha, + :date, :committer, :author, :diffs, :tree, :id, :stats, + :to_patch, to: :raw_commit + + class << self + def find_or_first(repo, commit_id = nil, root_ref) + commit = if commit_id + repo.commit(commit_id) + else + repo.commits(root_ref).first + end + + Gitlab::Git::Commit.new(commit) if commit + end + + def fresh_commits(repo, n = 10) + commits = repo.heads.map do |h| + repo.commits(h.name, n).map { |c| Gitlab::Git::Commit.new(c, h) } + end.flatten.uniq { |c| c.id } + + commits.sort! do |x, y| + y.committed_date <=> x.committed_date + end + + commits[0...n] + end + + def commits_with_refs(repo, n = 20) + commits = repo.branches.map { |ref| Gitlab::Git::Commit.new(ref.commit, ref) } + + commits.sort! do |x, y| + y.committed_date <=> x.committed_date + end + + commits[0..n] + end + + def commits_since(repo, date) + commits = repo.heads.map do |h| + repo.log(h.name, nil, since: date).each { |c| Gitlab::Git::Commit.new(c, h) } + end.flatten.uniq { |c| c.id } + + commits.sort! do |x, y| + y.committed_date <=> x.committed_date + end + + commits + end + + def commits(repo, ref, path = nil, limit = nil, offset = nil) + if path + repo.log(ref, path, max_count: limit, skip: offset) + elsif limit && offset + repo.commits(ref, limit, offset) + else + repo.commits(ref) + end.map{ |c| Gitlab::Git::Commit.new(c) } + end + + def commits_between(repo, from, to) + repo.commits_between(from, to).map { |c| Gitlab::Git::Commit.new(c) } + end + + def compare(project, from, to) + result = { + commits: [], + diffs: [], + commit: nil, + same: false + } + + return result unless from && to + + first = project.repository.commit(to.try(:strip)) + last = project.repository.commit(from.try(:strip)) + + if first && last + result[:same] = (first.id == last.id) + result[:commits] = project.repo.commits_between(last.id, first.id).map {|c| Gitlab::Git::Commit.new(c)} + + # Dont load diff for 100+ commits + result[:diffs] = if result[:commits].size > 100 + [] + else + project.repo.diff(last.id, first.id) rescue [] + end + + result[:commit] = Gitlab::Git::Commit.new(first) + end + + result + end + end + + def initialize(raw_commit, head = nil) + raise "Nil as raw commit passed" unless raw_commit + + @raw_commit = raw_commit + @head = head + end + + def short_id(length = 10) + id.to_s[0..length] + end + + def safe_message + @safe_message ||= message + end + + def created_at + committed_date + end + + def author_email + author.email + end + + def author_name + author.name + end + + # Was this commit committed by a different person than the original author? + def different_committer? + author_name != committer_name || author_email != committer_email + end + + def committer_name + committer.name + end + + def committer_email + committer.email + end + + def prev_commit + @prev_commit ||= if parents.present? + Gitlab::Git::Commit.new(parents.first) + else + nil + end + end + + def prev_commit_id + prev_commit.try :id + end + + # Shows the diff between the commit's parent and the commit. + # + # Cuts out the header and stats from #to_patch and returns only the diff. + def to_diff + # see Grit::Gitlab::Git::Commit#show + patch = to_patch + + # discard lines before the diff + lines = patch.split("\n") + while !lines.first.start_with?("diff --git") do + lines.shift + end + lines.pop if lines.last =~ /^[\d.]+$/ # Git version + lines.pop if lines.last == "-- " # end of diff + lines.join("\n") + end + + def has_zero_stats? + stats.total.zero? + rescue + true + end + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb new file mode 100644 index 00000000..53d9c735 --- /dev/null +++ b/lib/gitlab/git/repository.rb @@ -0,0 +1,181 @@ +# Gitlab::Git::Gitlab::Git::Commit is a wrapper around native Grit::Repository object +# We dont want to use grit objects inside app/ +# It helps us easily migrate to rugged in future +module Gitlab + module Git + class Repository + include Gitlab::Popen + + class NoRepository < StandardError; end + + # Repository directory name with namespace direcotry + # Examples: + # gitlab/gitolite + # diaspora + # + attr_accessor :path_with_namespace + + # Grit repo object + attr_accessor :repo + + # Default branch in the repository + attr_accessor :root_ref + + def initialize(path_with_namespace, root_ref = 'master') + @root_ref = root_ref || "master" + @path_with_namespace = path_with_namespace + + # Init grit repo object + repo + end + + def raw + repo + end + + def path_to_repo + @path_to_repo ||= File.join(Gitlab.config.gitlab_shell.repos_path, "#{path_with_namespace}.git") + end + + def repo + @repo ||= Grit::Repo.new(path_to_repo) + rescue Grit::NoSuchPathError + raise NoRepository.new('no repository for such path') + end + + def commit(commit_id = nil) + Gitlab::Git::Commit.find_or_first(repo, commit_id, root_ref) + end + + def fresh_commits(n = 10) + Gitlab::Git::Commit.fresh_commits(repo, n) + end + + def commits_with_refs(n = 20) + Gitlab::Git::Commit.commits_with_refs(repo, n) + end + + def commits_since(date) + Gitlab::Git::Commit.commits_since(repo, date) + end + + def commits(ref, path = nil, limit = nil, offset = nil) + Gitlab::Git::Commit.commits(repo, ref, path, limit, offset) + end + + def last_commit_for(ref, path = nil) + commits(ref, path, 1).first + end + + def commits_between(from, to) + Gitlab::Git::Commit.commits_between(repo, from, to) + end + + # Returns an Array of branch names + # sorted by name ASC + def branch_names + branches.map(&:name) + end + + # Returns an Array of Branches + def branches + repo.branches.sort_by(&:name) + end + + # Returns an Array of tag names + def tag_names + repo.tags.collect(&:name).sort.reverse + end + + # Returns an Array of Tags + def tags + repo.tags.sort_by(&:name).reverse + end + + # Returns an Array of branch and tag names + def ref_names + [branch_names + tag_names].flatten + end + + def heads + @heads ||= repo.heads + end + + def tree(fcommit, path = nil) + fcommit = commit if fcommit == :head + tree = fcommit.tree + path ? (tree / path) : tree + end + + def has_commits? + !!commit + rescue Grit::NoSuchPathError + false + end + + def empty? + !has_commits? + end + + # Discovers the default branch based on the repository's available branches + # + # - If no branches are present, returns nil + # - If one branch is present, returns its name + # - If two or more branches are present, returns the one that has a name + # matching root_ref (default_branch or 'master' if default_branch is nil) + def discover_default_branch + if branch_names.length == 0 + nil + elsif branch_names.length == 1 + branch_names.first + else + branch_names.select { |v| v == root_ref }.first + end + end + + # Archive Project to .tar.gz + # + # Already packed repo archives stored at + # app_root/tmp/repositories/project_name/project_name-commit-id.tag.gz + # + def archive_repo(ref) + ref = ref || self.root_ref + commit = self.commit(ref) + return nil unless commit + + # Build file path + file_name = self.path_with_namespace.gsub("/","_") + "-" + commit.id.to_s + ".tar.gz" + storage_path = Rails.root.join("tmp", "repositories") + file_path = File.join(storage_path, self.path_with_namespace, file_name) + + # Put files into a directory before archiving + prefix = File.basename(self.path_with_namespace) + "/" + + # Create file if not exists + unless File.exists?(file_path) + FileUtils.mkdir_p File.dirname(file_path) + file = self.repo.archive_to_file(ref, prefix, file_path) + end + + file_path + end + + # Return repo size in megabytes + # Cached in redis + def size + Rails.cache.fetch(cache_key(:size)) do + size = popen('du -s', path_to_repo).first.strip.to_i + (size.to_f / 1024).round(2) + end + end + + def expire_cache + Rails.cache.delete(cache_key(:size)) + end + + def cache_key(type) + "#{type}:#{path_with_namespace}" + end + end + end +end