Blob of app/models/repository.rb (raw blob data)

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 # TODO: cache
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 # build an initial empty set of 24 week commit data
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 # TODO: refactor into simpler approach
161 # TODO: caching
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 #[labels, data]
210 labels.inject({}) do |hash, label|
211 hash[label] = data[labels.index(label)]
212 hash
213 end
214 end
215
216 # Returns a Hash {email => user}, where email is selected from the +commits+
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