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