Add basics for private repositories
[gitorious:yousource.git] / app / models / repository.rb
1 # encoding: utf-8
2 #--
3 #   Copyright (C) 2010 Marko Peltola <marko@markopeltola.com>
4 #   Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies)
5 #   Copyright (C) 2007, 2008 Johan Sørensen <johan@johansorensen.com>
6 #   Copyright (C) 2008 David Chelimsky <dchelimsky@gmail.com>
7 #   Copyright (C) 2008 David A. Cuadrado <krawek@gmail.com>
8 #   Copyright (C) 2008 Tim Dysinger <tim@dysinger.net>
9 #   Copyright (C) 2008 David Aguilar <davvid@gmail.com>
10 #   Copyright (C) 2008 Tor Arne Vestbø <tavestbo@trolltech.com>
11 #   Copyright (C) 2009 Fabio Akita <fabio.akita@gmail.com>
12 #
13 #   This program is free software: you can redistribute it and/or modify
14 #   it under the terms of the GNU Affero General Public License as published by
15 #   the Free Software Foundation, either version 3 of the License, or
16 #   (at your option) any later version.
17 #
18 #   This program is distributed in the hope that it will be useful,
19 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
20 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 #   GNU Affero General Public License for more details.
22 #
23 #   You should have received a copy of the GNU Affero General Public License
24 #   along with this program.  If not, see <http://www.gnu.org/licenses/>.
25 #++
26
27 class Repository < ActiveRecord::Base
28   include ActiveMessaging::MessageSender
29   include RecordThrottling
30   include Watchable
31
32   KIND_PROJECT_REPO = 0
33   KIND_WIKI = 1
34   KIND_TEAM_REPO = 2
35   KIND_USER_REPO = 3
36   KIND_TRACKING_REPO = 4
37   KIND_SVN_MIRROR = 5  
38   KIND_ZIP_MIRROR = 6
39   KIND_PRIVATE_REPO = 7
40   KINDS_INTERNAL_REPO = [KIND_WIKI, KIND_TRACKING_REPO]
41
42   WIKI_NAME_SUFFIX = "-yousource-wiki"
43   WIKI_WRITABLE_EVERYONE = 0
44   WIKI_WRITABLE_PROJECT_MEMBERS = 1
45   SVN_MIRROR_NAME_SUFFIX = "-svn-mirror" 
46   ZIP_MIRROR_NAME_SUFFIX = "-zip-mirror"
47   REPO_VIEWABLE_EVERYONE = 1 # MP
48
49   belongs_to  :user
50   belongs_to  :project
51   belongs_to  :owner, :polymorphic => true
52   has_many    :committerships, :dependent => :destroy
53   belongs_to  :parent, :class_name => "Repository"
54   has_many    :clones, :class_name => "Repository", :foreign_key => "parent_id",
55     :dependent => :nullify
56   has_many    :comments, :as => :target, :dependent => :destroy
57   has_many    :merge_requests, :foreign_key => 'target_repository_id',
58     :order => "status, id desc", :dependent => :destroy
59   has_many    :proposed_merge_requests, :foreign_key => 'source_repository_id',
60                 :class_name => 'MergeRequest', :order => "id desc", :dependent => :destroy
61   has_many    :cloners, :dependent => :destroy
62   has_many    :events, :as => :target, :dependent => :destroy
63
64   NAME_FORMAT = /[a-z0-9_\-]+/i.freeze
65   validates_presence_of :user_id, :name, :owner_id, :project_id
66   validates_format_of :name, :with => /^#{NAME_FORMAT}$/i,
67     :message => "is invalid, must match something like /[a-z0-9_\\-]+/"
68   #validates_format_of :mirror_url, :with => /^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(([0-9]{1,5})?\/.*)?$/ix ###
69   validates_exclusion_of :name,
70     :in => (Gitorious::Reservations.project_names + Gitorious::Reservations.repository_names)
71   validates_uniqueness_of :name, :scope => :project_id, :case_sensitive => false
72   validates_uniqueness_of :hashed_path, :case_sensitive => false
73
74   before_validation :downcase_name
75   before_create :set_repository_hash
76   after_create :create_initial_committership
77   after_create :create_add_event_if_project_repo
78   after_create :post_repo_creation_message
79   after_create :add_owner_as_watchers
80   after_destroy :post_repo_deletion_message
81
82   throttle_records :create, :limit => 5,
83     :counter => proc{|record|
84       record.user.repositories.count(:all, :conditions => ["created_at > ?", 5.minutes.ago])
85     },
86     :conditions => proc{|record| {:user_id => record.user.id} },
87     :timeframe => 5.minutes
88
89   named_scope :by_users,  :conditions => { :kind => KIND_USER_REPO } do
90     def fresh(limit = 10)
91       find(:all, :order => "last_pushed_at DESC", :limit => limit)
92     end
93   end
94   named_scope :by_groups, :conditions => { :kind => KIND_TEAM_REPO } do
95     def fresh(limit=10)
96       find(:all, :order => "last_pushed_at DESC", :limit => limit)
97     end
98   end
99   named_scope :clones,    :conditions => ["kind in (?) and parent_id is not null",
100                                           [KIND_TEAM_REPO, KIND_USER_REPO]]
101   named_scope :mainlines, :conditions => ["kind in (?)", [KIND_PROJECT_REPO, KIND_SVN_MIRROR, KIND_ZIP_MIRROR]]  ###
102
103   named_scope :regular, :conditions => ["kind in (?)", [KIND_TEAM_REPO, KIND_USER_REPO,
104                                                        KIND_PROJECT_REPO, KIND_SVN_MIRROR, KIND_ZIP_MIRROR]]        ###
105   #named_scope :mirrors, conditions => ["kind in (?)", [KIND_SVN_MIRROR, KIND_ZIP_MIRROR]] ###
106
107   is_indexed :fields => ["name", "description"],
108     :include => [{
109       :association_name => "project",
110       :field => "slug",
111       :as => "project"
112     }], :conditions => "kind in (#{[KIND_PROJECT_REPO, KIND_TEAM_REPO, KIND_USER_REPO, KIND_SVN_MIRROR, KIND_ZIP_MIRROR].join(',')})"  ###
113
114   def self.human_name
115     I18n.t("activerecord.models.repository")
116   end
117
118   def self.new_by_cloning(other, username=nil)
119     suggested_name = username ? "#{username}s-#{other.name}" : nil
120     new(:parent => other, :project => other.project, :name => suggested_name,
121       :merge_requests_enabled => other.merge_requests_enabled)
122   end
123
124   def self.find_by_name_in_project!(name, containing_project = nil)
125     if containing_project
126       find_by_name_and_project_id!(name, containing_project.id)
127     else
128       find_by_name!(name)
129     end
130   end
131
132   def self.find_by_path(path)
133     base_path = path.gsub(/^#{Regexp.escape(GitoriousConfig['repository_base_path'])}/, "")
134     path_components = base_path.split("/").reject{|p| p.blank? }
135     repo_name, owner_name = [path_components.pop, path_components.shift]
136     project_name = path_components.pop
137     repo_name.sub!(/\.git/, "")
138
139     owner = case owner_name[0].chr
140       when "+"
141         Group.find_by_name!(owner_name.sub(/^\+/, ""))
142       when "~"
143         User.find_by_login!(owner_name.sub(/^~/, ""))
144       else
145         Project.find_by_slug!(owner_name)
146       end
147
148     if owner.is_a?(Project)
149       owner_conditions = { :project_id => owner.id }
150     else
151       owner_conditions = { :owner_type => owner.class.name, :owner_id => owner.id }
152     end
153     if project_name
154       if project = Project.find_by_slug(project_name)
155         owner_conditions.merge!(:project_id => project.id)
156       end
157     end
158     Repository.find(:first, :conditions => {:name => repo_name}.merge(owner_conditions))
159   end
160
161   def self.create_git_repository(path)
162     full_path = full_path_from_partial_path(path)
163     git_backend.create(full_path)
164
165     self.create_hooks(full_path)
166   end
167
168   # Updates repository contents with the contents of a zip,
169   # then creates and pushes a new commit.
170   def self.update_contents_from_zip(target_path, source_path)
171     full_path = full_path_from_partial_path(target_path)
172     
173     sp             = full_path.split("/")
174     repos_path     = sp[0, sp.size-3].join("/").sub(/\.git$/, "")
175
176     temp_clone_dir = sp.last.sub(/\.git$/, "")
177     full_temp_dir  = full_path_from_partial_path(temp_clone_dir)
178
179     sp2 = source_path.split("/")
180     source_package = sp2.last
181
182     Dir.chdir(repos_path) do
183       `git clone #{full_path}`
184       `wget      #{source_path}` 
185       `unzip -uo #{source_package} -d #{full_temp_dir}`
186     end
187     Dir.chdir(full_temp_dir) do
188       `git add .`
189       `git commit -am 'Update files by zip package'`
190       `git push #{full_path} master`
191     end
192     
193     # Remove temp dir and the zip
194     Dir.chdir(repos_path) do
195       `rm -rf #{temp_clone_dir}`
196       `rm     #{source_package}`
197     end
198   end
199
200   def self.create_git_repository_from_zip(target_path, source_path, options = {})
201     full_path = full_path_from_partial_path(target_path)
202     git_backend.create(full_path)   
203     self.update_contents_from_zip(target_path, source_path)
204     self.create_hooks(full_path)
205   end
206
207   def self.clone_git_repository(target_path, source_path, options = {})
208     full_path = full_path_from_partial_path(target_path)
209     Grit::Git.with_timeout(nil) do
210       git_backend.clone(full_path,
211         full_path_from_partial_path(source_path))
212     end
213     self.create_hooks(full_path) unless options[:skip_hooks]
214   end
215
216 ### CLONE SVN REPOSITORY ###
217   def self.clone_svn_repository(target_path, source_path, options = {})
218     full_path = full_path_from_partial_path(target_path)
219     sp = full_path.split("/")
220     repos_path = sp[0, sp.size-3].join("/").sub(/\.git$/, "") #going to the root repos directory
221     Dir.chdir(repos_path) do
222       `git svn clone #{source_path} #{target_path} --no-minimize-url --localtime`
223     end
224     Dir.chdir(full_path) do
225       `git svn show-ignore > .gitignore`
226     end
227     self.create_hooks(full_path)
228   end
229
230   def self.delete_git_repository(path)
231     git_backend.delete!(full_path_from_partial_path(path))
232   end
233
234 ### ACTIVE REPOS ###
235   # Returns the repositories limited by +limit+ who has the most activity within
236   # the +cutoff+ period
237   def self.most_active_recently(limit = 10, number_of_days = 3)
238     Rails.cache.fetch("repositories:most_active_recently:#{limit}:#{number_of_days}",
239         :expires_in => 30.minutes) do
240       find(:all, :joins => :events, :limit => limit,
241         :select => 'distinct repositories.*, count(events.id) as event_count',
242         :order => "event_count desc", :group => "repositories.id",
243         :conditions => ["events.created_at > ?", number_of_days.days.ago])
244     end
245   end
246
247
248   def self.most_active_clones_in_projects(projects, limit = 5)
249     key = "repository:most_active_clones_in_projects:#{projects.map(&:id).join('-')}:#{limit}"
250     Rails.cache.fetch(key, :expires_in => 2.hours) do
251       clone_ids = projects.map do |project|
252         project.repositories.clones.map{|r| r.id }
253       end.flatten
254       find(:all, :limit => limit,
255         :select => 'distinct repositories.*, count(events.id) as event_count',
256         :order => "event_count desc", :group => "repositories.id",
257         :conditions => ["repositories.id in (?) and events.created_at > ? and kind in (?)",
258                         clone_ids, 7.days.ago, [KIND_USER_REPO, KIND_TEAM_REPO]],
259         #:conditions => { :id => clone_ids },
260         :joins => :events, :include => :project)
261     end
262   end
263
264   def self.most_active_clones(limit = 10)
265     Rails.cache.fetch("repository:most_active_clones:#{limit}", :expires_in => 2.hours) do
266       find(:all, :limit => limit,
267         :select => 'distinct repositories.id, repositories.*, count(events.id) as event_count',
268         :order => "event_count desc", :group => "repositories.id",
269         :conditions => ["events.created_at > ? and kind in (?)",
270                         7.days.ago, [KIND_USER_REPO, KIND_TEAM_REPO]],
271         :joins => :events, :include => :project)
272     end
273   end
274
275   # Finds all repositories that might be due for a gc, starting with
276   # the ones who've been pushed to recently
277   def self.all_due_for_gc(batch_size = 25)
278     find(:all,
279       :order => "push_count_since_gc desc",
280       :conditions => "push_count_since_gc > 0",
281       :limit => batch_size)
282   end
283
284   def gitdir
285     "#{url_path}.git"
286   end
287
288   def url_path
289     if project_repo? || mirror_repo?     ###
290       File.join(project.to_param_with_prefix, name)
291     else
292       File.join(owner.to_param_with_prefix, project.slug, name)
293     end
294   end
295
296   def real_gitdir
297     "#{self.full_hashed_path}.git"
298   end
299
300   def clone_url
301     "git://#{GitoriousConfig['gitorious_host']}/#{gitdir}"
302   end
303
304   def http_clone_url
305     "http://git.#{GitoriousConfig['gitorious_host']}/#{gitdir}"
306   end
307
308   def http_cloning?
309     !GitoriousConfig["hide_http_clone_urls"]
310   end
311
312   def push_url
313     "#{GitoriousConfig['gitorious_user']}@#{GitoriousConfig['gitorious_host']}:#{gitdir}"
314   end
315
316   def full_repository_path
317     self.class.full_path_from_partial_path(real_gitdir)
318   end
319
320   def git
321     Grit::Repo.new(full_repository_path)
322   end
323
324   def has_commits?
325     return false if new_record? || !ready?
326     !git.heads.empty?
327   end
328
329   def getting_started_message?
330     return false if new_record? || !ready? || git.heads.empty?
331     return true if git.heads.length == 1 && git.heads.first.name == (GitoriousConfig["repo_meta_branch"])
332   end
333
334   def self.git_backend
335     RAILS_ENV == "test" ? MockGitBackend : GitBackend
336   end
337
338   def git_backend
339     RAILS_ENV == "test" ? MockGitBackend : GitBackend
340   end
341
342   def to_param
343     name
344   end
345
346   def to_xml(opts = {})
347     info_proc = Proc.new do |options|
348       builder = options[:builder]
349       builder.owner(owner.to_param, :kind => (owned_by_group? ? "Team" : "User"))
350       builder.kind(["mainline", "wiki", "team", "user", "mirror"][self.kind])     ###
351       builder.project(project.to_param)
352     end
353
354     super({
355       :procs => [info_proc],
356       :only => [:name, :created_at, :ready, :description, :last_pushed_at],
357       :methods => [:clone_url, :push_url, :parent]
358     }.merge(opts))
359   end
360
361   def head_candidate
362     return nil unless has_commits?
363     @head_candidate ||= head || git.heads.first
364   end
365
366   def head_candidate_name
367     if head = head_candidate
368       head.name
369     end
370   end
371
372   def head
373     git.head
374   end
375
376   def head=(head_name)
377     if new_head = git.heads.find{|h| h.name == head_name }
378       unless git.head == new_head
379         git.update_head(new_head)
380       end
381     end
382   end
383
384   def last_commit(ref = nil)
385     if has_commits?
386       @last_commit ||= Array(git.commits(ref || head_candidate.name, 1)).first
387     end
388     @last_commit
389   end
390
391   def commit_for_tree_path(ref, commit_id, path)
392     Rails.cache.fetch("treecommit:#{commit_id}:#{Digest::SHA1.hexdigest(ref+path)}") do
393       git.log(ref, path, {:max_count => 1}).first
394     end
395   end
396
397   def can_be_deleted_by?(candidate)
398     admin?(candidate)
399   end
400
401   # Can +a_user+ request a merge from this repository
402   def can_request_merge?(a_user)
403     !mainline? && writable_by?(a_user)
404   end
405
406 # MP
407   # Can +a_user+ view this repository
408   def can_view?(a_user)
409     if REPO_VIEWABLE_EVERYONE
410       return true
411     else
412       return viewer?(a_user)
413     end
414   end
415
416   # changes the owner to +another_owner+, removes the old owner as committer
417   # and adds +another_owner+ as committer
418   def change_owner_to!(another_owner)
419     unless owned_by_group?
420       transaction do
421         if existing = committerships.find_by_committer_id_and_committer_type(owner.id, owner.class.name)
422           existing.destroy
423         end
424         self.owner = another_owner
425         if self.kind != KIND_PROJECT_REPO || self.kind != KIND_SVN_MIRROR || self.kind != KIND_ZIP_MIRROR
426           case another_owner
427           when Group
428             self.kind = KIND_TEAM_REPO
429           when User
430             self.kind = KIND_USER_REPO
431           end
432         end
433         unless committerships.any?{|c|c.committer == another_owner}
434           committerships.create_for_owner!(self.owner)
435         end
436         save!
437         reload
438       end
439     end
440   end
441
442 ### FETCH REMOTE CHANGES FROM SVN REPOSITORY ###
443   def svn_fetch
444     Dir.chdir(self.full_repository_path) do
445       `git svn fetch --localtime`
446     end
447   end
448
449   def zip_update
450     return if !zip_mirror?
451     update_contents_from_zip(self.hashed_path, self.mirror_url)
452   end
453
454 ### add zip update method zip_mirror?
455   def update_mirror
456     if svn_mirror?
457       svn_fetch 
458     elsif zip_mirror?
459       zip_update
460     end
461   end
462
463 ### SVN MIRROR COMMAND ###
464   def post_repo_creation_message
465     return if tracking_repo?
466     options = {:target_class => self.class.name, :target_id => self.id}
467     options[:command] = parent ? 'clone_git_repository' : 'create_git_repository'
468     options[:arguments] = parent ? [real_gitdir, parent.real_gitdir] : [real_gitdir]
469
470     if self.svn_mirror? ### FIXME
471       options[:command] = 'clone_svn_repository' 
472       options[:arguments] = [real_gitdir, self.mirror_url]    
473     end 
474     if self.zip_mirror?
475       options[:command]   = 'create_git_repository_from_zip'
476       options[:arguments] = [real_gitdir, self.mirror_url]
477     end
478     
479     publish :create_repo, options.to_json
480   end
481
482   def post_repo_deletion_message
483     options = {:target_class => self.class.name, :command => 'delete_git_repository', :arguments => [real_gitdir]}
484     publish :destroy_repo, options.to_json
485   end
486
487   def total_commit_count
488     events.count(:conditions => {:action => Action::COMMIT})
489   end
490
491   def paginated_commits(ref, page, per_page = 30)
492     page    = (page || 1).to_i
493     begin
494       total = git.commit_count(ref)
495     rescue Grit::Git::GitTimeout
496       total = 2046
497     end
498     offset  = (page - 1) * per_page
499     commits = WillPaginate::Collection.new(page, per_page, total)
500     commits.replace git.commits(ref, per_page, offset)
501   end
502
503   def cached_paginated_commits(ref, page, per_page = 30)
504     page = (page || 1).to_i
505     last_commit_id = last_commit(ref) ? last_commit(ref).id : nil
506     total = Rails.cache.fetch("paglogtotal:#{self.id}:#{last_commit_id}:#{ref}") do
507       begin
508         git.commit_count(ref)
509       rescue Grit::Git::GitTimeout
510         2046
511       end
512     end
513     Rails.cache.fetch("paglog:#{page}:#{self.id}:#{last_commit_id}:#{ref}") do
514       offset = (page - 1) * per_page
515       commits = WillPaginate::Collection.new(page, per_page, total)
516       commits.replace git.commits(ref, per_page, offset)
517     end
518   end
519
520   def count_commits_from_last_week_by_user(user)
521     return 0 unless has_commits?
522
523     commits_by_email = git.commits_since("master", "last week").collect do |commit|
524       commit.committer.email == user.email
525     end
526     commits_by_email.size
527   end
528
529   # TODO: cache
530   def commit_graph_data(head = "master")
531     commits = git.commits_since(head, "24 weeks ago")
532     commits_by_week = commits.group_by{|c| c.committed_date.strftime("%W") }
533
534     # build an initial empty set of 24 week commit data
535     weeks = [1.day.from_now-1.week]
536     23.times{|w| weeks << weeks.last-1.week }
537     week_numbers = weeks.map{|d| d.strftime("%W") }
538     commits = (0...24).to_a.map{|i| 0 }
539
540     commits_by_week.each do |week, commits_in_week|
541       if week_pos = week_numbers.index(week)
542         commits[week_pos+1] = commits_in_week.size
543       end
544     end
545     commits = [] if commits.max == 0
546     [week_numbers.reverse, commits.reverse]
547   end
548
549   # TODO: caching
550   def commit_graph_data_by_author(head = "master")
551     h = {}
552     emails = {}
553     data = self.git.git.shortlog({:e => true, :s => true }, head)
554     data.each_line do |line|
555       count, actor = line.split("\t")
556       actor = Grit::Actor.from_string(actor)
557
558       h[actor.name] ||= 0
559       h[actor.name] += count.to_i
560       emails[actor.email] = actor.name
561     end
562
563     users = User.find(:all, :conditions => ["email in (?)", emails.keys])
564     users.each do |user|
565       author_name = emails[user.email]
566       if h[author_name] # in the event that a user with the same name has used two different emails, he'd be gone by now
567         h[user.login] = h.delete(author_name)
568       end
569     end
570
571     h
572   end
573
574   # Returns a Hash {email => user}, where email is selected from the +commits+
575   def users_by_commits(commits)
576     emails = commits.map { |commit| commit.author.email }.uniq
577     users = User.find(:all, :conditions => ["email in (?)", emails])
578
579     users_by_email = users.inject({}){|hash, user| hash[user.email] = user; hash }
580     users_by_email
581   end
582
583   def cloned_from(ip, country_code = "--", country_name = nil, protocol = 'git')
584     cloners.create(:ip => ip, :date => Time.now.utc, :country_code => country_code, :country => country_name, :protocol => protocol)
585   end
586
587   def wiki?
588     kind == KIND_WIKI
589   end
590
591   def project_repo?
592     kind == KIND_PROJECT_REPO
593   end
594
595 ### MODIFIED TO PLACE SVN MIRROR TO MAINLINE ###
596   def mainline?
597     project_repo? || mirror_repo?
598   end
599
600   def team_repo?
601     kind == KIND_TEAM_REPO
602   end
603
604   def user_repo?
605     kind == KIND_USER_REPO
606   end
607
608   def tracking_repo?
609     kind == KIND_TRACKING_REPO
610   end
611
612   def mirror_repo?
613     svn_mirror? || zip_mirror?
614   end
615
616 ### SVN MIRROR KIND ### 
617   def svn_mirror?
618     kind == KIND_SVN_MIRROR
619   end
620
621   def zip_mirror?
622     kind == KIND_ZIP_MIRROR
623   end
624
625 =begin
626 ### SVN URL ASSIGN -> UPDATE GIT CONFIG ###
627   def mirror_url=(url)
628     Dir.chdir(self.full_repository_path) do
629       `git config svn-remote.svn.url #{url}`
630     end
631   end
632 =end
633
634   # returns an array of users who have commit bits to this repository either
635   # directly through the owner, or "indirectly" through the associated groups
636   def committers
637     committerships.committers.map{|c| c.members }.flatten.compact.uniq
638   end
639
640   # Returns a list of Users who can review things (as per their Committership)
641   def reviewers
642     committerships.reviewers.map{|c| c.members }.flatten.compact.uniq
643   end
644
645 # MP
646   # Returns a list of Users who can see this repo
647   def viewers
648     committerships.viewers.map{ |c| c.members }.flatten.compact.uniq
649   end
650
651   # The list of users who can admin this repo, either directly as
652   # committerships or indirectly as members of a group
653   def administrators
654     committerships.admins.map{|c| c.members }.flatten.compact.uniq
655   end
656
657   def committer?(a_user)
658     a_user.is_a?(User) ? self.committers.include?(a_user) : false
659   end
660
661   def reviewer?(a_user)
662     a_user.is_a?(User) ? self.reviewers.include?(a_user) : false
663   end
664
665 # MP
666   def viewer?(a_user)
667     a_user.is_a?(User) ? self.viewers.include?(a_user) : false
668   end
669
670   def admin?(a_user)
671     a_user.is_a?(User) ? self.administrators.include?(a_user) : false
672   end
673
674   # Is this repo writable by +a_user+, eg. does he have push permissions here
675   # NOTE: this may be context-sensitive depending on the kind of repo
676   def writable_by?(a_user)
677     if wiki?
678       wiki_writable_by?(a_user)
679     else
680       committers.include?(a_user)
681     end
682   end
683
684   def owned_by_group?
685     owner === Group
686   end
687
688   def breadcrumb_parent
689     if mainline?
690       project
691     else
692       owner
693     end
694   end
695
696   def title
697     name
698   end  
699
700   def owner_title
701     mainline? ? project.title : owner.title
702   end
703
704 ### MODIFIED TO USE SVN MIRROR REPOS AS PROJECT REPOS ###
705   # returns the project if it's a KIND_PROJECT_REPO, otherwise the owner
706   def project_or_owner
707     if project_repo? || mirror_repo?
708       project
709     else
710       owner
711     end
712     #project_repo? ? project : owner
713   end
714
715   def full_hashed_path
716     hashed_path || set_repository_hash
717   end
718
719   # Returns a list of users being either the owner (if User) or each admin member (if Group)
720   def owners
721     result = if owned_by_group?
722       owner.members.select do |member|
723         owner.admin?(member)
724       end
725     else
726       [owner]
727     end
728     return result
729   end
730
731   def set_repository_hash
732     self.hashed_path ||= begin
733       raw_hash = Digest::SHA1.hexdigest(owner.to_param +
734                                         self.to_param +
735                                         Time.now.to_f.to_s +
736                                         ActiveSupport::SecureRandom.hex)
737       sharded_hash = sharded_hashed_path(raw_hash)
738       sharded_hash
739     end
740   end
741
742   # Creates a block within which we generate events for each attribute changed
743   # as long as it's changed to a legal value
744   def log_changes_with_user(a_user)
745     @updated_fields = []
746     yield
747     log_updates(a_user)
748   end
749
750   # Replaces a value within a log_changes_with_user block
751   def replace_value(field, value, allow_blank = false)
752     old_value = read_attribute(field)
753     if !allow_blank && value.blank? || old_value == value
754       return
755     end
756     self.send("#{field}=", value)
757     valid?
758     if !errors.on(field)
759       @updated_fields << field
760     end
761   end
762
763   # Logs events that occured within a log_changes_with_user block
764   def log_updates(a_user)
765     @updated_fields.each do |field_name|
766       events.build(:action => Action::UPDATE_REPOSITORY, :user => a_user, :project => project, :body => "Changed the repository #{field_name.to_s}")
767     end
768   end
769
770   def requires_signoff_on_merge_requests?
771     mainline? && project.merge_requests_need_signoff?
772   end
773
774   def build_tracking_repository
775     result = Repository.new(:parent => self, :user => user, :owner => owner, :kind => KIND_TRACKING_REPO, :name => "tracking_repository_for_#{id}", :project => project)
776     return result
777   end
778
779   def create_tracking_repository
780     result = build_tracking_repository
781     result.save!
782     return result
783   end
784
785   def tracking_repository
786     self.class.find(:first, :conditions => {:parent_id => self, :kind => KIND_TRACKING_REPO})
787   end
788
789   def has_tracking_repository?
790     !tracking_repository.nil?
791   end
792
793   def merge_request_status_tags
794     # Note: we use 'as raw_status_tag' since the
795     # MergeRequest#status_tag is overridden
796     result = MergeRequest.find_by_sql(["SELECT status_tag as raw_status_tag
797       FROM merge_requests
798       WHERE target_repository_id = ?
799       GROUP BY status_tag", self.id]).collect{|mr| mr.raw_status_tag }
800     result.compact
801   end
802
803   # Fallback when the real sequence number is taken
804   def calculate_highest_merge_request_sequence_number
805     merge_requests.maximum(:sequence_number)
806   end
807
808   # The basis for the sequence number, reflects the number of merge requests
809   def merge_request_count
810     merge_requests.count
811   end
812
813   # Ideally we want to reflect how many merge requests are entered.
814   # However, if this sequence is taken (an old record), we'll go one up
815   # from the highest number instead
816   def next_merge_request_sequence_number
817     candidate = merge_request_count + 1
818     if merge_requests.find_by_sequence_number(candidate)
819       calculate_highest_merge_request_sequence_number + 1
820     else
821       candidate
822     end
823   end
824
825   # Runs git-gc on this repository, and updates the last_gc_at attribute
826   def gc!
827     Grit::Git.with_timeout(nil) do
828       if self.git.gc_auto
829         self.last_gc_at = Time.now
830         self.push_count_since_gc = 0
831         return save
832       end
833     end
834   end
835
836   def register_push
837     self.last_pushed_at = Time.now.utc
838     self.push_count_since_gc = push_count_since_gc.to_i + 1
839   end
840   
841   def update_disk_usage
842     self.disk_usage = calculate_disk_usage
843   end
844
845   def calculate_disk_usage
846     @calculated_disk_usage ||= `du -sb #{full_repository_path} 2>/dev/null`.chomp.to_i
847   end
848
849   def matches_regexp?(term)
850     return user.login =~ term ||
851       name =~ term ||
852       (owned_by_group? ? owner.name =~ term : false) ||
853       description =~ term
854   end
855
856   def search_clones(term)
857     self.class.title_search(term, "parent_id", id)
858   end
859
860   # Searches for term in
861   # - title
862   # - description
863   # - owner name/login
864   #
865   # Scoped to column +key+ having +value+
866   #
867   # Example:
868   #   title_search("foo", "parent_id", 1) #  will find clones of Repo with id 1
869   #                                          matching 'foo'
870   #
871   #   title_search("foo", "project_id", 1) # will find repositories in Project#1
872   #                                          matching 'foo'
873   def self.title_search(term, key, value)
874     sql = "SELECT repositories.* FROM repositories
875       INNER JOIN users on repositories.user_id=users.id
876       INNER JOIN groups on repositories.owner_id=groups.id
877       WHERE repositories.#{key}=#{value}
878       AND (repositories.name LIKE :q OR repositories.description LIKE :q OR groups.name LIKE :q)
879       AND repositories.owner_type='Group'
880       AND kind in (:kinds)
881       UNION ALL
882       SELECT repositories.* from repositories
883       INNER JOIN users on repositories.user_id=users.id
884       INNER JOIN users owners on repositories.owner_id=owners.id
885       WHERE repositories.#{key}=#{value}
886       AND (repositories.name LIKE :q OR repositories.description LIKE :q OR owners.login LIKE :q)
887       AND repositories.owner_type='User'
888       AND kind in (:kinds)"
889     self.find_by_sql([sql, {:q => "%#{term}%",
890                         :id => value,
891                         :kinds =>
892                         [KIND_TEAM_REPO, KIND_USER_REPO, KIND_PROJECT_REPO, KIND_SVN_MIRROR, KIND_ZIP_MIRROR]}])
893   end
894
895
896 # MP
897     def private_repo
898       false
899     end
900
901
902
903
904   protected
905     def sharded_hashed_path(h)
906       first = h[0,3]
907       second = h[3,3]
908       last = h[-34, 34]
909       "#{first}/#{second}/#{last}"
910     end
911
912     def create_initial_committership
913       self.committerships.create_for_owner!(self.owner)
914     end
915
916     def self.full_path_from_partial_path(path)
917       File.expand_path(File.join(GitoriousConfig["repository_base_path"], path))
918     end
919
920     def downcase_name
921       name.downcase! if name
922     end
923
924     def create_add_event_if_project_repo
925       if project_repo? || mirror_repo?
926         #(action_id, target, user, data = nil, body = nil, date = Time.now.utc)
927         self.project.create_event(Action::ADD_PROJECT_REPOSITORY, self, self.user,
928               nil, nil, date = created_at)        
929       end
930     end
931
932     # Special permission check for KIND_WIKI repositories
933     def wiki_writable_by?(a_user)
934       case self.wiki_permissions
935       when WIKI_WRITABLE_EVERYONE
936         return true
937       when WIKI_WRITABLE_PROJECT_MEMBERS
938         return self.project.member?(a_user)
939       end
940     end
941
942     def add_owner_as_watchers
943       return if KINDS_INTERNAL_REPO.include?(self.kind)
944       watched_by!(user)
945     end
946
947   private
948   def self.create_hooks(path)
949     hooks = File.join(GitoriousConfig["repository_base_path"], ".hooks")
950     Dir.chdir(path) do
951       hooks_base_path = File.expand_path("#{RAILS_ROOT}/data/hooks")
952
953       if not File.symlink?(hooks)
954         if not File.exist?(hooks)
955           FileUtils.ln_s(hooks_base_path, hooks) # Create symlink
956         end
957       elsif File.expand_path(File.readlink(hooks)) != hooks_base_path
958         FileUtils.ln_sf(hooks_base_path, hooks) # Fixup symlink
959       end
960     end
961
962     local_hooks = File.join(path, "hooks")
963     unless File.exist?(local_hooks)
964       target_path = Pathname.new(hooks).relative_path_from(Pathname.new(path))
965       Dir.chdir(path) do
966         FileUtils.ln_s(target_path, "hooks")
967       end
968     end
969
970     File.open(File.join(path, "description"), "w") do |file|
971       sp = path.split("/")
972       file << sp[sp.size-1, sp.size].join("/").sub(/\.git$/, "") << "\n"
973     end
974
975   end
976
977 end