| 1 |
class Repository < ActiveRecord::Base |
| 2 |
belongs_to :user |
| 3 |
belongs_to :project |
| 4 |
belongs_to :parent, :class_name => "Repository" |
| 5 |
has_many :committerships, :dependent => :destroy |
| 6 |
has_many :committers, :through => :committerships, :source => :user |
| 7 |
has_many :comments, :dependent => :destroy |
| 8 |
has_many :merge_requests, :foreign_key => 'target_repository_id', |
| 9 |
:order => "status, id desc", :dependent => :destroy |
| 10 |
has_many :proposed_merge_requests, :foreign_key => 'source_repository_id', |
| 11 |
:class_name => 'MergeRequest', :order => "id desc", :dependent => :destroy |
| 12 |
|
| 13 |
validates_presence_of :user_id, :project_id, :name |
| 14 |
validates_format_of :name, :with => /^[a-z0-9_\-]+$/i, |
| 15 |
:message => "is invalid, must match something like /[a-z0-9_\\-]+/" |
| 16 |
validates_uniqueness_of :name, :scope => :project_id, :case_sensitive => false |
| 17 |
|
| 18 |
before_save :set_as_mainline_if_first |
| 19 |
after_create :add_user_as_committer, :create_new_repos_task |
| 20 |
after_destroy :create_delete_repos_task |
| 21 |
|
| 22 |
def self.new_by_cloning(other, username=nil) |
| 23 |
suggested_name = username ? "#{username}s-clone" : nil |
| 24 |
new(:parent => other, :project => other.project, :name => suggested_name) |
| 25 |
end |
| 26 |
|
| 27 |
def self.find_by_name!(name) |
| 28 |
find_by_name(name) || raise(ActiveRecord::RecordNotFound) |
| 29 |
end |
| 30 |
|
| 31 |
def self.create_git_repository(path) |
| 32 |
git_backend.create(full_path_from_partial_path(path)) |
| 33 |
end |
| 34 |
|
| 35 |
def self.clone_git_repository(target_path, source_path) |
| 36 |
git_backend.clone(full_path_from_partial_path(target_path), |
| 37 |
full_path_from_partial_path(source_path)) |
| 38 |
end |
| 39 |
|
| 40 |
def self.delete_git_repository(path) |
| 41 |
git_backend.delete!(full_path_from_partial_path(path)) |
| 42 |
end |
| 43 |
|
| 44 |
def gitdir |
| 45 |
File.join(project.slug, "#{name}.git") |
| 46 |
end |
| 47 |
|
| 48 |
def clone_url |
| 49 |
"git://#{GitoriousConfig['gitorious_host']}/#{gitdir}" |
| 50 |
end |
| 51 |
|
| 52 |
def http_clone_url |
| 53 |
"http://git.#{GitoriousConfig['gitorious_host']}/#{gitdir}" |
| 54 |
end |
| 55 |
|
| 56 |
def push_url |
| 57 |
"git@#{GitoriousConfig['gitorious_host']}:#{gitdir}" |
| 58 |
end |
| 59 |
|
| 60 |
def full_repository_path |
| 61 |
self.class.full_path_from_partial_path(gitdir) |
| 62 |
end |
| 63 |
|
| 64 |
def git |
| 65 |
Grit::Repo.new(full_repository_path) |
| 66 |
end |
| 67 |
|
| 68 |
def has_commits? |
| 69 |
return false if new_record? || !ready? |
| 70 |
!git.heads.empty? |
| 71 |
end |
| 72 |
|
| 73 |
def self.git_backend |
| 74 |
RAILS_ENV == "test" ? MockGitBackend : GitBackend |
| 75 |
end |
| 76 |
|
| 77 |
def git_backend |
| 78 |
RAILS_ENV == "test" ? MockGitBackend : GitBackend |
| 79 |
end |
| 80 |
|
| 81 |
def to_param |
| 82 |
name |
| 83 |
end |
| 84 |
|
| 85 |
def to_xml(opts = {}) |
| 86 |
super({:methods => [:gitdir, :clone_url, :push_url]}.merge(opts)) |
| 87 |
end |
| 88 |
|
| 89 |
def add_committer(user) |
| 90 |
unless user.can_write_to?(self) |
| 91 |
committers << user |
| 92 |
end |
| 93 |
end |
| 94 |
|
| 95 |
def head_candidate |
| 96 |
return nil unless has_commits? |
| 97 |
@head_candidate ||= git.heads.find{|h| h.name == "master"} || git.heads.first |
| 98 |
end |
| 99 |
|
| 100 |
def last_commit |
| 101 |
if has_commits? |
| 102 |
@last_commit ||= git.commits(head_candidate.name, 1).first |
| 103 |
end |
| 104 |
@last_commit |
| 105 |
end |
| 106 |
|
| 107 |
def can_be_deleted_by?(candidate) |
| 108 |
!mainline? && (candidate == user) |
| 109 |
end |
| 110 |
|
| 111 |
def create_new_repos_task |
| 112 |
Task.create!(:target_class => self.class.name, |
| 113 |
:command => parent ? "clone_git_repository" : "create_git_repository", |
| 114 |
:arguments => parent ? [gitdir, parent.gitdir] : [gitdir], |
| 115 |
:target_id => self.id) |
| 116 |
end |
| 117 |
|
| 118 |
def create_delete_repos_task |
| 119 |
Task.create!(:target_class => self.class.name, |
| 120 |
:command => "delete_git_repository", :arguments => [gitdir]) |
| 121 |
end |
| 122 |
|
| 123 |
def paginated_commits(tree_name, page, per_page = 30) |
| 124 |
page = (page || 1).to_i |
| 125 |
total = git.commit_count(tree_name) |
| 126 |
offset = (page - 1) * per_page |
| 127 |
commits = WillPaginate::Collection.new(page, per_page, total) |
| 128 |
commits.replace git.commits(tree_name, per_page, offset) |
| 129 |
end |
| 130 |
|
| 131 |
def count_commits_from_last_week_by_user(user) |
| 132 |
return 0 unless has_commits? |
| 133 |
|
| 134 |
commits_by_email = git.commits_since("master", "last week").collect do |commit| |
| 135 |
commit.committer.email == user.email |
| 136 |
end |
| 137 |
commits_by_email.size |
| 138 |
end |
| 139 |
|
| 140 |
|
| 141 |
def commit_graph_data(head = "master") |
| 142 |
commits = git.commits_since(head, "24 weeks ago") |
| 143 |
commits_by_week = commits.group_by{|c| c.committed_date.strftime("%W") } |
| 144 |
|
| 145 |
|
| 146 |
weeks = [1.day.from_now-1.week] |
| 147 |
23.times{|w| weeks << weeks.last-1.week } |
| 148 |
week_numbers = weeks.map{|d| d.strftime("%W") } |
| 149 |
commits = (0...24).to_a.map{|i| 0 } |
| 150 |
|
| 151 |
commits_by_week.each do |week, commits_in_week| |
| 152 |
if week_pos = week_numbers.index(week) |
| 153 |
commits[week_pos+1] = commits_in_week.size |
| 154 |
end |
| 155 |
end |
| 156 |
commits = [] if commits.max == 0 |
| 157 |
[week_numbers.reverse, commits.reverse] |
| 158 |
end |
| 159 |
|
| 160 |
|
| 161 |
|
| 162 |
def commit_graph_data_by_author(head = "master") |
| 163 |
h = Hash.new |
| 164 |
|
| 165 |
data = self.git.git.rev_list({:pretty => "format:name:%cn", :since => "1 years ago" }, head) |
| 166 |
data.each_line do |line| |
| 167 |
if line =~ /^name:(.*)$/ then |
| 168 |
author = $1 |
| 169 |
|
| 170 |
if h[author] |
| 171 |
h[author] += 1 |
| 172 |
else |
| 173 |
h[author] = 1 |
| 174 |
end |
| 175 |
end |
| 176 |
end |
| 177 |
|
| 178 |
sorted = h.sort_by do |author, commits| |
| 179 |
commits |
| 180 |
end |
| 181 |
|
| 182 |
labels = [] |
| 183 |
data = [] |
| 184 |
|
| 185 |
max = 8 |
| 186 |
others = [] |
| 187 |
top = sorted |
| 188 |
|
| 189 |
|
| 190 |
if sorted.size > max |
| 191 |
top = sorted[sorted.size-max, sorted.size] |
| 192 |
others = sorted[0, sorted.size-max] |
| 193 |
end |
| 194 |
|
| 195 |
top.each do |entry| |
| 196 |
author = entry.first |
| 197 |
v = entry.last |
| 198 |
|
| 199 |
data << v |
| 200 |
labels << author |
| 201 |
end |
| 202 |
|
| 203 |
unless others.empty? |
| 204 |
others_v = others.inject { |v, acum| [v.last + acum.last] } |
| 205 |
labels << "others" |
| 206 |
data << others_v.last |
| 207 |
end |
| 208 |
|
| 209 |
|
| 210 |
labels.inject({}) do |hash, label| |
| 211 |
hash[label] = data[labels.index(label)] |
| 212 |
hash |
| 213 |
end |
| 214 |
end |
| 215 |
|
| 216 |
|
| 217 |
def users_by_commits(commits) |
| 218 |
emails = commits.map { |commit| commit.author.email }.uniq |
| 219 |
users = User.find(:all, :conditions => ["email in (?)", emails]) |
| 220 |
|
| 221 |
users_by_email = users.inject({}){|hash, user| hash[user.email] = user; hash } |
| 222 |
users_by_email |
| 223 |
end |
| 224 |
|
| 225 |
protected |
| 226 |
def set_as_mainline_if_first |
| 227 |
unless project.repositories.size >= 1 |
| 228 |
self.mainline = true |
| 229 |
end |
| 230 |
end |
| 231 |
|
| 232 |
def add_user_as_committer |
| 233 |
committers << user |
| 234 |
end |
| 235 |
|
| 236 |
def self.full_path_from_partial_path(path) |
| 237 |
File.expand_path(File.join(GitoriousConfig["repository_base_path"], path)) |
| 238 |
end |
| 239 |
end |