Refactor project membership and private repo functionality into generic protected...
[gitorious:mainline.git] / app / models / repository.rb
1 # encoding: utf-8
2 #--
3 #   Copyright (C) 2012 Gitorious AS
4 #   Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies)
5 #   Copyright (C) 2009 Fabio Akita <fabio.akita@gmail.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) 2007, 2008 Johan Sørensen <johan@johansorensen.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 require "gitorious/reservations"
27 require "gitorious/messaging"
28
29 class Repository < ActiveRecord::Base
30   include Gitorious::Messaging::Publisher
31   include RecordThrottling
32   include Watchable
33   include Gitorious::Search
34   include Gitorious::Authorization
35   include Gitorious::Protectable
36
37   KIND_PROJECT_REPO = 0
38   KIND_WIKI = 1
39   KIND_TEAM_REPO = 2
40   KIND_USER_REPO = 3
41   KIND_TRACKING_REPO = 4
42   KINDS_INTERNAL_REPO = [KIND_WIKI, KIND_TRACKING_REPO]
43
44   WIKI_NAME_SUFFIX = "-gitorious-wiki"
45   WIKI_WRITABLE_EVERYONE = 0
46   WIKI_WRITABLE_PROJECT_MEMBERS = 1
47
48   belongs_to  :user
49   belongs_to  :project
50   belongs_to  :owner, :polymorphic => true
51   has_many    :repository_memberships, :as => :content
52   has_many    :content_memberships, :as => :content
53   has_many    :committerships, :dependent => :destroy
54   belongs_to  :parent, :class_name => "Repository"
55   has_many    :clones, :class_name => "Repository", :foreign_key => "parent_id",
56     :dependent => :nullify
57   has_many    :comments, :as => :target, :dependent => :destroy
58   has_many    :merge_requests, :foreign_key => "target_repository_id",
59     :order => "status, id desc", :dependent => :destroy
60   has_many    :proposed_merge_requests, :foreign_key => "source_repository_id",
61                 :class_name => "MergeRequest", :order => "id desc", :dependent => :destroy
62   has_many    :cloners, :dependent => :destroy
63   has_many    :events, :as => :target, :dependent => :destroy
64   has_many :hooks, :dependent => :destroy
65
66   NAME_FORMAT = /[a-z0-9_\-]+/i.freeze
67   validates_presence_of :user_id, :name, :owner_id, :project_id
68   validates_format_of :name, :with => /^#{NAME_FORMAT}$/i,
69     :message => "is invalid, must match something like /[a-z0-9_\\-]+/"
70   validates_exclusion_of :name,
71     :in => (Gitorious::Reservations.project_names + Gitorious::Reservations.repository_names)
72   validates_uniqueness_of :name, :scope => :project_id, :case_sensitive => false
73   validates_uniqueness_of :hashed_path, :case_sensitive => false
74
75   before_validation :downcase_name
76   before_create :set_repository_hash
77   after_create :create_initial_committership
78   after_create :create_add_event_if_project_repo
79   after_create :post_repo_creation_message
80   after_create :add_owner_as_watchers
81   after_destroy :post_repo_deletion_message
82
83   throttle_records :create, :limit => 5,
84     :counter => proc{|record|
85       record.user.repositories.count(:all, :conditions => ["created_at > ?", 5.minutes.ago])
86     },
87     :conditions => proc{|record| {:user_id => record.user.id} },
88     :timeframe => 5.minutes
89
90   named_scope :by_users,  :conditions => { :kind => KIND_USER_REPO } do
91     def fresh(limit = 10)
92       find(:all, :order => "last_pushed_at DESC", :limit => limit)
93     end
94   end
95   named_scope :by_groups, :conditions => { :kind => KIND_TEAM_REPO } do
96     def fresh(limit=10)
97       find(:all, :order => "last_pushed_at DESC", :limit => limit)
98     end
99   end
100   named_scope :clones,    :conditions => ["kind in (?) and parent_id is not null",
101                                           [KIND_TEAM_REPO, KIND_USER_REPO]]
102   named_scope :mainlines, :conditions => { :kind => KIND_PROJECT_REPO }
103
104   named_scope :regular, :conditions => ["kind in (?)", [KIND_TEAM_REPO, KIND_USER_REPO,
105                                                        KIND_PROJECT_REPO]]
106   is_indexed do |s|
107     s.index :name
108     s.index :description
109     s.index "project#slug", :as => :project
110     s.conditions "kind in (#{[KIND_PROJECT_REPO, KIND_TEAM_REPO, KIND_USER_REPO].join(',')})"
111   end
112
113   def self.human_name
114     I18n.t("activerecord.models.repository")
115   end
116
117   def self.new_by_cloning(other, username=nil)
118     suggested_name = username ? "#{username}s-#{other.name}" : nil
119     new(:parent => other, :project => other.project, :name => suggested_name,
120       :merge_requests_enabled => other.merge_requests_enabled)
121   end
122
123   def self.find_by_name_in_project!(name, containing_project = nil)
124     if containing_project
125       find_by_name_and_project_id!(name, containing_project.id)
126     else
127       find_by_name!(name)
128     end
129   end
130
131   def self.find_by_path(path)
132     base_path = path.gsub(/^#{Regexp.escape(GitoriousConfig['repository_base_path'])}/, "")
133     path_components = base_path.split("/").reject{|p| p.blank? }
134     repo_name, owner_name = [path_components.pop, path_components.shift]
135     project_name = path_components.pop
136     repo_name.sub!(/\.git/, "")
137
138     owner = case owner_name[0].chr
139       when "+"
140         Group.find_by_name!(owner_name.sub(/^\+/, ""))
141       when "~"
142         User.find_by_login!(owner_name.sub(/^~/, ""))
143       else
144         Project.find_by_slug!(owner_name)
145       end
146
147     if owner.is_a?(Project)
148       owner_conditions = { :project_id => owner.id }
149     else
150       owner_conditions = { :owner_type => owner.class.name, :owner_id => owner.id }
151     end
152     if project_name
153       if project = Project.find_by_slug(project_name)
154         owner_conditions.merge!(:project_id => project.id)
155       end
156     end
157     Repository.find(:first, :conditions => {:name => repo_name}.merge(owner_conditions))
158   end
159
160   def self.create_git_repository(path)
161     full_path = full_path_from_partial_path(path)
162     git_backend.create(full_path)
163
164     self.create_hooks(full_path)
165   end
166
167   def self.clone_git_repository(target_path, source_path, options = {})
168     full_path = full_path_from_partial_path(target_path)
169     Grit::Git.with_timeout(nil) do
170       git_backend.clone(full_path,
171         full_path_from_partial_path(source_path))
172     end
173     self.create_hooks(full_path) unless options[:skip_hooks]
174   end
175
176   def self.delete_git_repository(path)
177     git_backend.delete!(full_path_from_partial_path(path))
178   end
179
180   def self.most_active_clones_in_projects(projects, limit = 5)
181     key = "repository:most_active_clones_in_projects:#{projects.map(&:id).join('-')}:#{limit}"
182     Rails.cache.fetch(key, :expires_in => 2.hours) do
183       clone_ids = projects.map do |project|
184         project.repositories.clones.map{|r| r.id }
185       end.flatten
186       find(:all, :limit => limit,
187         :select => "distinct repositories.*, count(events.id) as event_count",
188         :order => "event_count desc", :group => "repositories.id",
189         :conditions => ["repositories.id in (?) and events.created_at > ? and kind in (?)",
190                         clone_ids, 7.days.ago, [KIND_USER_REPO, KIND_TEAM_REPO]],
191         #:conditions => { :id => clone_ids },
192         :joins => :events, :include => :project)
193     end
194   end
195
196   def self.most_active_clones(limit = 10)
197     Rails.cache.fetch("repository:most_active_clones:#{limit}", :expires_in => 2.hours) do
198       find(:all, :limit => limit,
199         :select => "distinct repositories.id, repositories.*, count(events.id) as event_count",
200         :order => "event_count desc", :group => "repositories.id",
201         :conditions => ["events.created_at > ? and kind in (?)",
202                         7.days.ago, [KIND_USER_REPO, KIND_TEAM_REPO]],
203         :joins => :events, :include => :project)
204     end
205   end
206
207   # Finds all repositories that might be due for a gc, starting with
208   # the ones who've been pushed to recently
209   def self.all_due_for_gc(batch_size = 25)
210     find(:all,
211       :order => "push_count_since_gc desc",
212       :conditions => "push_count_since_gc > 0",
213       :limit => batch_size)
214   end
215
216   def gitdir
217     "#{url_path}.git"
218   end
219
220   def url_path
221     if project_repo?
222       File.join(project.to_param_with_prefix, name)
223     else
224       File.join(owner.to_param_with_prefix, project.slug, name)
225     end
226   end
227
228   def real_gitdir
229     "#{self.full_hashed_path}.git"
230   end
231
232   def browse_url
233     "#{GitoriousConfig['scheme']}://#{GitoriousConfig['gitorious_host']}/#{url_path}"
234   end
235
236   def default_clone_url
237     return git_clone_url if git_cloning?
238     return http_clone_url if http_cloning?
239     ssh_clone_url
240   end
241
242   def clone_url
243     "git://#{GitoriousConfig['gitorious_host']}/#{gitdir}"
244   end
245
246   def ssh_clone_url
247     push_url
248   end
249
250   def git_clone_url
251     clone_url
252   end
253
254   def http_clone_url
255     "#{GitoriousConfig['scheme']}://#{Site::HTTP_CLONING_SUBDOMAIN}.#{GitoriousConfig['gitorious_host']}/#{gitdir}"
256   end
257
258   def http_cloning?
259     !GitoriousConfig["hide_http_clone_urls"]
260   end
261
262   def git_cloning?
263     !GitoriousConfig["hide_git_clone_urls"]
264   end
265
266   def ssh_cloning?
267     true
268   end
269
270   def push_url
271     "#{GitoriousConfig['gitorious_user']}@#{GitoriousConfig['gitorious_host']}:#{gitdir}"
272   end
273
274   def display_ssh_url?(user)
275     return true if GitoriousConfig["always_display_ssh_url"]
276     can_push?(user, self)
277   end
278
279   def full_repository_path
280     self.class.full_path_from_partial_path(real_gitdir)
281   end
282
283   def git
284     Grit::Repo.new(full_repository_path)
285   end
286
287   def has_commits?
288     return false if new_record? || !ready?
289     !git.heads.empty?
290   end
291
292   def self.git_backend
293     RAILS_ENV == "test" ? MockGitBackend : GitBackend
294   end
295
296   def git_backend
297     RAILS_ENV == "test" ? MockGitBackend : GitBackend
298   end
299
300   def to_param
301     name
302   end
303
304   def to_xml(opts = {})
305     info_proc = Proc.new do |options|
306       builder = options[:builder]
307       builder.owner(owner.to_param, :kind => (owned_by_group? ? "Team" : "User"))
308       builder.kind(["mainline", "wiki", "team", "user"][self.kind])
309       builder.project(project.to_param)
310     end
311
312     super({
313       :procs => [info_proc],
314       :only => [:name, :created_at, :ready, :description, :last_pushed_at],
315       :methods => [:clone_url, :push_url, :parent]
316     }.merge(opts))
317   end
318
319   def head_candidate
320     return nil unless has_commits?
321     @head_candidate ||= head || git.heads.first
322   end
323
324   def head_candidate_name
325     if head = head_candidate
326       head.name
327     end
328   end
329
330   def head
331     git.head
332   end
333
334   def head=(head_name)
335     if new_head = git.heads.find{|h| h.name == head_name }
336       unless git.head == new_head
337         git.update_head(new_head)
338       end
339     end
340   end
341
342   def last_commit(ref = nil)
343     if has_commits?
344       @last_commit ||= Array(git.commits(ref || head_candidate.name, 1)).first
345     end
346     @last_commit
347   end
348
349   def commit_for_tree_path(ref, commit_id, path)
350     Rails.cache.fetch("treecommit:#{commit_id}:#{Digest::SHA1.hexdigest(ref+path)}") do
351       git.log(ref, path, {:max_count => 1}).first
352     end
353   end
354
355   # changes the owner to +another_owner+, removes the old owner as committer
356   # and adds +another_owner+ as committer
357   def change_owner_to!(another_owner)
358     unless owned_by_group?
359       transaction do
360         if existing = committerships.find_by_committer_id_and_committer_type(owner.id, owner.class.name)
361           existing.destroy
362         end
363         self.owner = another_owner
364         if self.kind != KIND_PROJECT_REPO
365           case another_owner
366           when Group
367             self.kind = KIND_TEAM_REPO
368           when User
369             self.kind = KIND_USER_REPO
370           end
371         end
372         unless committerships.any?{|c|c.committer == another_owner}
373           committerships.create_for_owner!(self.owner)
374         end
375         save!
376         reload
377       end
378     end
379   end
380
381   def post_repo_creation_message
382     return if tracking_repo?
383
384     payload = {
385       :target_class => self.class.name,
386       :target_id => self.id,
387       :command => parent ? "clone_git_repository" : "create_git_repository",
388       :arguments => parent ? [real_gitdir, parent.real_gitdir] : [real_gitdir]
389     }
390
391     publish("/queue/GitoriousRepositoryCreation", payload)
392   end
393
394   def post_repo_deletion_message
395     payload = {
396       :target_class => self.class.name,
397       :command => "delete_git_repository",
398       :arguments => [real_gitdir]
399     }
400
401     publish("/queue/GitoriousRepositoryDeletion", payload)
402   end
403
404   def total_commit_count
405     events.count(:conditions => {:action => Action::COMMIT})
406   end
407
408   def paginated_commits(ref, page, per_page = 30)
409     page    = (page || 1).to_i
410     begin
411       total = git.commit_count(ref)
412     rescue Grit::Git::GitTimeout
413       total = 2046
414     end
415     offset  = (page - 1) * per_page
416     commits = WillPaginate::Collection.new(page, per_page, total)
417     commits.replace git.commits(ref, per_page, offset)
418   end
419
420   def cached_paginated_commits(ref, page, per_page = 30)
421     page = (page || 1).to_i
422     last_commit_id = last_commit(ref) ? last_commit(ref).id : nil
423     total = Rails.cache.fetch("paglogtotal:#{self.id}:#{last_commit_id}:#{ref}") do
424       begin
425         git.commit_count(ref)
426       rescue Grit::Git::GitTimeout
427         2046
428       end
429     end
430     Rails.cache.fetch("paglog:#{page}:#{self.id}:#{last_commit_id}:#{ref}") do
431       offset = (page - 1) * per_page
432       commits = WillPaginate::Collection.new(page, per_page, total)
433       commits.replace git.commits(ref, per_page, offset)
434     end
435   end
436
437   def count_commits_from_last_week_by_user(user)
438     return 0 unless has_commits?
439
440     commits_by_email = git.commits_since("master", "last week").collect do |commit|
441       commit.committer.email == user.email
442     end
443     commits_by_email.size
444   end
445
446   # TODO: cache
447   def commit_graph_data(head = "master")
448     commits = git.commits_since(head, "24 weeks ago")
449     commits_by_week = commits.group_by{|c| c.committed_date.strftime("%W") }
450
451     # build an initial empty set of 24 week commit data
452     weeks = [1.day.from_now-1.week]
453     23.times{|w| weeks << weeks.last-1.week }
454     week_numbers = weeks.map{|d| d.strftime("%W") }
455     commits = (0...24).to_a.map{|i| 0 }
456
457     commits_by_week.each do |week, commits_in_week|
458       if week_pos = week_numbers.index(week)
459         commits[week_pos+1] = commits_in_week.size
460       end
461     end
462     commits = [] if commits.max == 0
463     [week_numbers.reverse, commits.reverse]
464   end
465
466   # TODO: caching
467   def commit_graph_data_by_author(head = "master")
468     h = {}
469     emails = {}
470     data = self.git.git.shortlog({:e => true, :s => true }, head)
471     data.each_line do |line|
472       count, actor = line.split("\t")
473       actor = Grit::Actor.from_string(actor)
474
475       h[actor.name] ||= 0
476       h[actor.name] += count.to_i
477       emails[actor.email] = actor.name
478     end
479
480     users = User.find(:all, :conditions => ["email in (?)", emails.keys])
481     users.each do |user|
482       author_name = emails[user.email]
483       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
484         h[user.login] = h.delete(author_name)
485       end
486     end
487
488     h
489   end
490
491   # Returns a Hash {email => user}, where email is selected from the +commits+
492   def self.users_by_commits(commits)
493     emails = commits.map { |commit| commit.author.email }.uniq
494     users = User.find(:all, :conditions => ["email in (?)", emails])
495
496     users_by_email = users.inject({}){|hash, user| hash[user.email] = user; hash }
497     users_by_email
498   end
499
500   def cloned_from(ip, country_code = "--", country_name = nil, protocol = "git")
501     cloners.create(:ip => ip, :date => Time.now.utc, :country_code => country_code, :country => country_name, :protocol => protocol)
502   end
503
504   def wiki?
505     kind == KIND_WIKI
506   end
507
508   def project_repo?
509     kind == KIND_PROJECT_REPO
510   end
511
512   def mainline?
513     project_repo?
514   end
515
516   def team_repo?
517     kind == KIND_TEAM_REPO
518   end
519
520   def user_repo?
521     kind == KIND_USER_REPO
522   end
523
524   def tracking_repo?
525     kind == KIND_TRACKING_REPO
526   end
527
528   def owned_by_group?
529     owner === Group
530   end
531
532   def breadcrumb_parent
533     if mainline?
534       project
535     else
536       owner
537     end
538   end
539
540   def title
541     name
542   end
543
544   def owner_title
545     mainline? ? project.title : owner.title
546   end
547
548   # returns the project if it's a KIND_PROJECT_REPO, otherwise the owner
549   def project_or_owner
550     project_repo? ? project : owner
551   end
552
553   def full_hashed_path
554     hashed_path || set_repository_hash
555   end
556
557   # Returns a list of users being either the owner (if User) or each admin member (if Group)
558   def owners
559     result = if owned_by_group?
560       owner.members.select do |member|
561         if owner.respond_to?(:admin)
562           admin?(member, owner)
563         else
564           admin?(member, owner)
565         end
566       end
567     else
568       [owner]
569     end
570     return result
571   end
572
573   def set_repository_hash
574     self.hashed_path ||= begin
575       raw_hash = Digest::SHA1.hexdigest(owner.to_param +
576                                         self.to_param +
577                                         Time.now.to_f.to_s +
578                                         ActiveSupport::SecureRandom.hex)
579       sharded_hash = sharded_hashed_path(raw_hash)
580       sharded_hash
581     end
582   end
583
584   # Creates a block within which we generate events for each attribute changed
585   # as long as it's changed to a legal value
586   def log_changes_with_user(a_user)
587     @updated_fields = []
588     yield
589     log_updates(a_user)
590   end
591
592   # Replaces a value within a log_changes_with_user block
593   def replace_value(field, value, allow_blank = false)
594     old_value = read_attribute(field)
595     if !allow_blank && value.blank? || old_value == value
596       return
597     end
598     self.send("#{field}=", value)
599     valid?
600     if !errors.on(field)
601       @updated_fields << field
602     end
603   end
604
605   # Logs events that occured within a log_changes_with_user block
606   def log_updates(a_user)
607     @updated_fields.each do |field_name|
608       events.build(:action => Action::UPDATE_REPOSITORY, :user => a_user, :project => project, :body => "Changed the repository #{field_name.to_s}")
609     end
610   end
611
612   def requires_signoff_on_merge_requests?
613     mainline? && project.merge_requests_need_signoff?
614   end
615
616   def build_tracking_repository
617     result = Repository.new(:parent => self, :user => user, :owner => owner, :kind => KIND_TRACKING_REPO, :name => "tracking_repository_for_#{id}", :project => project)
618     return result
619   end
620
621   def create_tracking_repository
622     result = build_tracking_repository
623     result.save!
624     return result
625   end
626
627   def tracking_repository
628     self.class.find(:first, :conditions => {:parent_id => self, :kind => KIND_TRACKING_REPO})
629   end
630
631   def has_tracking_repository?
632     !tracking_repository.nil?
633   end
634
635   def merge_request_status_tags
636     # Note: we use 'as raw_status_tag' since the
637     # MergeRequest#status_tag is overridden
638     result = MergeRequest.find_by_sql(["SELECT status_tag as raw_status_tag
639       FROM merge_requests
640       WHERE target_repository_id = ?
641       GROUP BY status_tag", self.id]).collect{|mr| mr.raw_status_tag }
642     result.compact
643   end
644
645   # Fallback when the real sequence number is taken
646   def calculate_highest_merge_request_sequence_number
647     merge_requests.maximum(:sequence_number)
648   end
649
650   # The basis for the sequence number, reflects the number of merge requests
651   def merge_request_count
652     merge_requests.count
653   end
654
655   # Ideally we want to reflect how many merge requests are entered.
656   # However, if this sequence is taken (an old record), we'll go one up
657   # from the highest number instead
658   def next_merge_request_sequence_number
659     candidate = merge_request_count + 1
660     if merge_requests.find_by_sequence_number(candidate)
661       calculate_highest_merge_request_sequence_number + 1
662     else
663       candidate
664     end
665   end
666
667   # Runs git-gc on this repository, and updates the last_gc_at attribute
668   def gc!
669     Grit::Git.with_timeout(nil) do
670       if self.git.gc_auto
671         self.last_gc_at = Time.now
672         self.push_count_since_gc = 0
673         return save
674       end
675     end
676   end
677
678   def register_push
679     self.last_pushed_at = Time.now.utc
680     self.push_count_since_gc = push_count_since_gc.to_i + 1
681     update_disk_usage
682   end
683
684   def update_disk_usage
685     self.disk_usage = calculate_disk_usage
686   end
687
688   def calculate_disk_usage
689     @calculated_disk_usage ||= `du -sb #{full_repository_path} 2>/dev/null`.chomp.to_i
690   end
691
692   def matches_regexp?(term)
693     return user.login =~ term ||
694       name =~ term ||
695       (owned_by_group? ? owner.name =~ term : false) ||
696       description =~ term
697   end
698
699   def search_clones(term)
700     self.class.title_search(term, "parent_id", id)
701   end
702
703   # Searches for term in
704   # - title
705   # - description
706   # - owner name/login
707   #
708   # Scoped to column +key+ having +value+
709   #
710   # Example:
711   #   title_search("foo", "parent_id", 1) #  will find clones of Repo with id 1
712   #                                          matching 'foo'
713   #
714   #   title_search("foo", "project_id", 1) # will find repositories in Project#1
715   #                                          matching 'foo'
716   def self.title_search(term, key, value)
717     sql = "SELECT repositories.* FROM repositories
718       INNER JOIN users on repositories.user_id=users.id
719       INNER JOIN groups on repositories.owner_id=groups.id
720       WHERE repositories.#{key}=#{value}
721       AND (repositories.name LIKE :q OR repositories.description LIKE :q OR groups.name LIKE :q)
722       AND repositories.owner_type='Group'
723       AND kind in (:kinds)
724       UNION ALL
725       SELECT repositories.* from repositories
726       INNER JOIN users on repositories.user_id=users.id
727       INNER JOIN users owners on repositories.owner_id=owners.id
728       WHERE repositories.#{key}=#{value}
729       AND (repositories.name LIKE :q OR repositories.description LIKE :q OR owners.login LIKE :q)
730       AND repositories.owner_type='User'
731       AND kind in (:kinds)"
732     self.find_by_sql([sql, {:q => "%#{term}%",
733                         :id => value,
734                         :kinds =>
735                         [KIND_TEAM_REPO, KIND_USER_REPO, KIND_PROJECT_REPO]}])
736   end
737
738   protected
739   def sharded_hashed_path(h)
740     first = h[0,3]
741     second = h[3,3]
742     last = h[-34, 34]
743     "#{first}/#{second}/#{last}"
744   end
745
746   def create_initial_committership
747     self.committerships.create_for_owner!(self.owner)
748   end
749
750   def self.full_path_from_partial_path(path)
751     File.expand_path(File.join(GitoriousConfig["repository_base_path"], path))
752   end
753
754   def downcase_name
755     name.downcase! if name
756   end
757
758   def create_add_event_if_project_repo
759     if project_repo?
760       #(action_id, target, user, data = nil, body = nil, date = Time.now.utc)
761       self.project.create_event(Action::ADD_PROJECT_REPOSITORY, self, self.user,
762                                 nil, nil, date = created_at)
763     end
764   end
765
766   def add_owner_as_watchers
767     return if KINDS_INTERNAL_REPO.include?(self.kind)
768     watched_by!(user)
769   end
770
771   private
772   def self.create_hooks(path)
773     hooks = File.join(GitoriousConfig["repository_base_path"], ".hooks")
774     Dir.chdir(path) do
775       hooks_base_path = File.expand_path("#{RAILS_ROOT}/data/hooks")
776
777       if not File.symlink?(hooks)
778         if not File.exist?(hooks)
779           FileUtils.ln_s(hooks_base_path, hooks) # Create symlink
780         end
781       elsif File.expand_path(File.readlink(hooks)) != hooks_base_path
782         FileUtils.ln_sf(hooks_base_path, hooks) # Fixup symlink
783       end
784     end
785
786     local_hooks = File.join(path, "hooks")
787     unless File.exist?(local_hooks)
788       target_path = Pathname.new(hooks).relative_path_from(Pathname.new(path))
789       Dir.chdir(path) do
790         FileUtils.ln_s(target_path, "hooks")
791       end
792     end
793
794     File.open(File.join(path, "description"), "w") do |file|
795       sp = path.split("/")
796       file << sp[sp.size-1, sp.size].join("/").sub(/\.git$/, "") << "\n"
797     end
798   end
799 end