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 has_many :cloners, :dependent => :destroy
13 has_many :events, :as => :target, :dependent => :destroy
14
15 validates_presence_of :user_id, :project_id, :name
16 validates_format_of :name, :with => /^[a-z0-9_\-]+$/i,
17 :message => "is invalid, must match something like /[a-z0-9_\\-]+/"
18 validates_uniqueness_of :name, :scope => :project_id, :case_sensitive => false
19
20 before_save :set_as_mainline_if_first
21 after_create :add_user_as_committer, :create_new_repos_task
22 after_destroy :create_delete_repos_task
23
24 def self.new_by_cloning(other, username=nil)
25 suggested_name = username ? "#{username}s-clone" : nil
26 new(:parent => other, :project => other.project, :name => suggested_name)
27 end
28
29 def self.find_by_name!(name)
30 find_by_name(name) || raise(ActiveRecord::RecordNotFound)
31 end
32
33 def self.find_by_path(path)
34 base_path = path.gsub(/^#{Regexp.escape(GitoriousConfig['repository_base_path'])}/, "")
35 repo_name, project_name = base_path.split("/").reverse
36
37 project = Project.find_by_slug!(project_name)
38 project.repositories.find_by_name(repo_name.sub(/\.git/, ""))
39 end
40
41 def self.create_git_repository(path)
42 full_path = full_path_from_partial_path(path)
43 git_backend.create(full_path)
44
45 self.create_hooks(full_path)
46 end
47
48 def self.clone_git_repository(target_path, source_path)
49 full_path = full_path_from_partial_path(target_path)
50 git_backend.clone(full_path,
51 full_path_from_partial_path(source_path))
52
53 self.create_hooks(full_path)
54 end
55
56 def self.delete_git_repository(path)
57 git_backend.delete!(full_path_from_partial_path(path))
58 end
59
60 def gitdir
61 File.join(project.slug, "#{name}.git")
62 end
63
64 def clone_url
65 "git://#{GitoriousConfig['gitorious_host']}/#{gitdir}"
66 end
67
68 def http_clone_url
69 "http://git.#{GitoriousConfig['gitorious_host']}/#{gitdir}"
70 end
71
72 def push_url
73 "git@#{GitoriousConfig['gitorious_host']}:#{gitdir}"
74 end
75
76 def full_repository_path
77 self.class.full_path_from_partial_path(gitdir)
78 end
79
80 def git
81 Grit::Repo.new(full_repository_path)
82 end
83
84 def has_commits?
85 return false if new_record? || !ready?
86 !git.heads.empty?
87 end
88
89 def self.git_backend
90 RAILS_ENV == "test" ? MockGitBackend : GitBackend
91 end
92
93 def git_backend
94 RAILS_ENV == "test" ? MockGitBackend : GitBackend
95 end
96
97 def to_param
98 name
99 end
100
101 def to_xml(opts = {})
102 super({:methods => [:gitdir, :clone_url, :push_url]}.merge(opts))
103 end
104
105 def add_committer(user)
106 unless user.can_write_to?(self)
107 committers << user
108 end
109 end
110
111 def head_candidate
112 return nil unless has_commits?
113 @head_candidate ||= git.heads.find{|h| h.name == "master"} || git.heads.first
114 end
115
116 def last_commit
117 if has_commits?
118 @last_commit ||= git.commits(head_candidate.name, 1).first
119 end
120 @last_commit
121 end
122
123 def can_be_deleted_by?(candidate)
124 !mainline? && (candidate == user)
125 end
126
127 def create_new_repos_task
128 Task.create!(:target_class => self.class.name,
129 :command => parent ? "clone_git_repository" : "create_git_repository",
130 :arguments => parent ? [gitdir, parent.gitdir] : [gitdir],
131 :target_id => self.id)
132 end
133
134 def create_delete_repos_task
135 Task.create!(:target_class => self.class.name,
136 :command => "delete_git_repository", :arguments => [gitdir])
137 end
138
139 def total_commit_count
140 events.count(:conditions => {:action => Action::COMMIT})
141 end
142
143 def paginated_commits(tree_name, page, per_page = 30)
144 page = (page || 1).to_i
145 total = git.commit_count(tree_name)
146 offset = (page - 1) * per_page
147 commits = WillPaginate::Collection.new(page, per_page, total)
148 commits.replace git.commits(tree_name, per_page, offset)
149 end
150
151 def count_commits_from_last_week_by_user(user)
152 return 0 unless has_commits?
153
154 commits_by_email = git.commits_since("master", "last week").collect do |commit|
155 commit.committer.email == user.email
156 end
157 commits_by_email.size
158 end
159
160 # TODO: cache
161 def commit_graph_data(head = "master")
162 commits = git.commits_since(head, "24 weeks ago")
163 commits_by_week = commits.group_by{|c| c.committed_date.strftime("%W") }
164
165 # build an initial empty set of 24 week commit data
166 weeks = [1.day.from_now-1.week]
167 23.times{|w| weeks << weeks.last-1.week }
168 week_numbers = weeks.map{|d| d.strftime("%W") }
169 commits = (0...24).to_a.map{|i| 0 }
170
171 commits_by_week.each do |week, commits_in_week|
172 if week_pos = week_numbers.index(week)
173 commits[week_pos+1] = commits_in_week.size
174 end
175 end
176 commits = [] if commits.max == 0
177 [week_numbers.reverse, commits.reverse]
178 end
179
180 # TODO: caching
181 def commit_graph_data_by_author(head = "master")
182 h = {}
183 emails = {}
184 count_author_regexp = /^\s+(\d+)\s(.+)\s\<(\S+)\>$/.freeze
185 data = self.git.git.shortlog({:e => true, :s => true }, head)
186 data.each_line do |line|
187 if line =~ count_author_regexp
188 count = $1.to_i
189 author = $2
190 email = $3
191
192 h[author] ||= 0
193 h[author] += count
194
195 emails[email] = author
196 end
197 end
198
199 users = User.find(:all, :conditions => ["email in (?)", emails.keys])
200 users.each do |user|
201 author_name = emails[user.email]
202 h[user.login] = h.delete(author_name)
203 end
204
205 h
206 end
207
208 # Returns a Hash {email => user}, where email is selected from the +commits+
209 def users_by_commits(commits)
210 emails = commits.map { |commit| commit.author.email }.uniq
211 users = User.find(:all, :conditions => ["email in (?)", emails])
212
213 users_by_email = users.inject({}){|hash, user| hash[user.email] = user; hash }
214 users_by_email
215 end
216
217
218 def cloned_from(ip, country_code = "--", country_name = nil)
219 cloners.create(:ip => ip, :date => Time.now.utc, :country_code => country_code, :country => country_name)
220 end
221
222 protected
223 def set_as_mainline_if_first
224 unless project.repositories.size >= 1
225 self.mainline = true
226 end
227 end
228
229 def add_user_as_committer
230 committers << user
231 end
232
233 def self.full_path_from_partial_path(path)
234 File.expand_path(File.join(GitoriousConfig["repository_base_path"], path))
235 end
236
237 private
238 def self.create_hooks(path)
239 hooks = File.join(GitoriousConfig["repository_base_path"], ".hooks")
240 Dir.chdir(path) do
241 hooks_base_path = File.expand_path("#{RAILS_ROOT}/data/hooks")
242
243 if not File.symlink?(hooks)
244 if not File.exist?(hooks)
245 FileUtils.ln_s(hooks_base_path, hooks) # Create symlink
246 end
247 elsif File.expand_path(File.readlink(hooks)) != hooks_base_path
248 FileUtils.ln_sf(hooks_base_path, hooks) # Fixup symlink
249 end
250 end
251
252 local_hooks = File.join(path, "hooks")
253 unless File.exist?(local_hooks)
254 target_path = Pathname.new(hooks).relative_path_from(Pathname.new(path))
255 Dir.chdir(path) do
256 FileUtils.ln_s(target_path, "hooks")
257 end
258 end
259
260 File.open(File.join(path, "description"), "w") do |file|
261 sp = path.split("/")
262 file << sp[sp.size-1, sp.size].join("/").sub(/\.git$/, "") << "\n"
263 end
264 end
265 end