OSDN Git Service

Move files search to gitlab_git
[wvm/gitlab.git] / app / models / project.rb
1 # == Schema Information
2 #
3 # Table name: projects
4 #
5 #  id                     :integer          not null, primary key
6 #  name                   :string(255)
7 #  path                   :string(255)
8 #  description            :text
9 #  created_at             :datetime         not null
10 #  updated_at             :datetime         not null
11 #  creator_id             :integer
12 #  default_branch         :string(255)
13 #  issues_enabled         :boolean          default(TRUE), not null
14 #  wall_enabled           :boolean          default(TRUE), not null
15 #  merge_requests_enabled :boolean          default(TRUE), not null
16 #  wiki_enabled           :boolean          default(TRUE), not null
17 #  namespace_id           :integer
18 #  public                 :boolean          default(FALSE), not null
19 #  issues_tracker         :string(255)      default("gitlab"), not null
20 #  issues_tracker_id      :string(255)
21 #  snippets_enabled       :boolean          default(TRUE), not null
22 #  last_activity_at       :datetime
23 #
24
25 require "grit"
26
27 class Project < ActiveRecord::Base
28   include Gitlab::ShellAdapter
29   extend Enumerize
30
31   attr_accessible :name, :path, :description, :default_branch, :issues_tracker, :label_list,
32     :issues_enabled, :wall_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id,
33     :wiki_enabled, :public, :import_url, :last_activity_at, as: [:default, :admin]
34
35   attr_accessible :namespace_id, :creator_id, as: :admin
36
37   acts_as_taggable_on :labels, :issues_default_labels
38
39   attr_accessor :import_url
40
41   # Relations
42   belongs_to :creator,      foreign_key: "creator_id", class_name: "User"
43   belongs_to :group,        foreign_key: "namespace_id", conditions: "type = 'Group'"
44   belongs_to :namespace
45
46   has_one :last_event, class_name: 'Event', order: 'events.created_at DESC', foreign_key: 'project_id'
47   has_one :gitlab_ci_service, dependent: :destroy
48   has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
49   has_one :forked_from_project, through: :forked_project_link
50
51   has_many :events,             dependent: :destroy
52   has_many :merge_requests,     dependent: :destroy
53   has_many :issues,             dependent: :destroy, order: "state DESC, created_at DESC"
54   has_many :milestones,         dependent: :destroy
55   has_many :users_projects,     dependent: :destroy
56   has_many :notes,              dependent: :destroy
57   has_many :snippets,           dependent: :destroy
58   has_many :hooks,              dependent: :destroy, class_name: "ProjectHook"
59   has_many :protected_branches, dependent: :destroy
60   has_many :user_team_project_relationships, dependent: :destroy
61
62   has_many :users,          through: :users_projects
63   has_many :user_teams,     through: :user_team_project_relationships
64   has_many :user_team_user_relationships, through: :user_teams
65   has_many :user_teams_members, through: :user_team_user_relationships
66
67   has_many :deploy_keys_projects, dependent: :destroy
68   has_many :deploy_keys, through: :deploy_keys_projects
69
70   delegate :name, to: :owner, allow_nil: true, prefix: true
71
72   # Validations
73   validates :creator, presence: true
74   validates :description, length: { within: 0..2000 }
75   validates :name, presence: true, length: { within: 0..255 },
76             format: { with: Gitlab::Regex.project_name_regex,
77                       message: "only letters, digits, spaces & '_' '-' '.' allowed. Letter should be first" }
78   validates :path, presence: true, length: { within: 0..255 },
79             format: { with: Gitlab::Regex.path_regex,
80                       message: "only letters, digits & '_' '-' '.' allowed. Letter should be first" }
81   validates :issues_enabled, :wall_enabled, :merge_requests_enabled,
82             :wiki_enabled, inclusion: { in: [true, false] }
83   validates :issues_tracker_id, length: { within: 0..255 }
84
85   validates_uniqueness_of :name, scope: :namespace_id
86   validates_uniqueness_of :path, scope: :namespace_id
87
88   validates :import_url,
89     format: { with: URI::regexp(%w(http https)), message: "should be a valid url" },
90     if: :import?
91
92   validate :check_limit, :repo_name
93
94   # Scopes
95   scope :without_user, ->(user)  { where("projects.id NOT IN (:ids)", ids: user.authorized_projects.map(&:id) ) }
96   scope :without_team, ->(team) { team.projects.present? ? where("projects.id NOT IN (:ids)", ids: team.projects.map(&:id)) : scoped  }
97   scope :not_in_group, ->(group) { where("projects.id NOT IN (:ids)", ids: group.project_ids ) }
98   scope :in_team, ->(team) { where("projects.id IN (:ids)", ids: team.projects.map(&:id)) }
99   scope :in_namespace, ->(namespace) { where(namespace_id: namespace.id) }
100   scope :in_group_namespace, -> { joins(:group) }
101   scope :sorted_by_activity, -> { order("projects.last_activity_at DESC") }
102   scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
103   scope :joined, ->(user) { where("namespace_id != ?", user.namespace_id) }
104   scope :public_only, -> { where(public: true) }
105
106   enumerize :issues_tracker, in: (Gitlab.config.issues_tracker.keys).append(:gitlab), default: :gitlab
107
108   class << self
109     def abandoned
110       project_ids = Event.select('max(created_at) as latest_date, project_id').
111         group('project_id').
112         having('latest_date < ?', 6.months.ago).map(&:project_id)
113
114       where(id: project_ids)
115     end
116
117     def with_push
118       includes(:events).where('events.action = ?', Event::PUSHED)
119     end
120
121     def active
122       joins(:issues, :notes, :merge_requests).order("issues.created_at, notes.created_at, merge_requests.created_at DESC")
123     end
124
125     def search query
126       where("projects.name LIKE :query OR projects.path LIKE :query", query: "%#{query}%")
127     end
128
129     def find_with_namespace(id)
130       if id.include?("/")
131         id = id.split("/")
132         namespace = Namespace.find_by_path(id.first)
133         return nil unless namespace
134
135         where(namespace_id: namespace.id).find_by_path(id.second)
136       else
137         where(path: id, namespace_id: nil).last
138       end
139     end
140
141     def access_options
142       UsersProject.access_roles
143     end
144   end
145
146   def team
147     @team ||= ProjectTeam.new(self)
148   end
149
150   def repository
151     @repository ||= Repository.new(path_with_namespace, default_branch)
152   end
153
154   def saved?
155     id && persisted?
156   end
157
158   def import?
159     import_url.present?
160   end
161
162   def check_limit
163     unless creator.can_create_project?
164       errors[:limit_reached] << ("Your own projects limit is #{creator.projects_limit}! Please contact administrator to increase it")
165     end
166   rescue
167     errors[:base] << ("Can't check your ability to create project")
168   end
169
170   def repo_name
171     denied_paths = %w(admin dashboard groups help profile projects search)
172
173     if denied_paths.include?(path)
174       errors.add(:path, "like #{path} is not allowed")
175     end
176   end
177
178   def to_param
179     if namespace
180       namespace.path + "/" + path
181     else
182       path
183     end
184   end
185
186   def web_url
187     [Gitlab.config.gitlab.url, path_with_namespace].join("/")
188   end
189
190   def build_commit_note(commit)
191     notes.new(commit_id: commit.id, noteable_type: "Commit")
192   end
193
194   def last_activity
195     last_event
196   end
197
198   def last_activity_date
199     last_activity_at || updated_at
200   end
201
202   def project_id
203     self.id
204   end
205
206   def issues_labels
207     @issues_labels ||= (issues_default_labels + issues.tags_on(:labels)).uniq.sort_by(&:name)
208   end
209
210   def issue_exists?(issue_id)
211     if used_default_issues_tracker?
212       self.issues.where(id: issue_id).first.present?
213     else
214       true
215     end
216   end
217
218   def used_default_issues_tracker?
219     self.issues_tracker == Project.issues_tracker.default_value
220   end
221
222   def can_have_issues_tracker_id?
223     self.issues_enabled && !self.used_default_issues_tracker?
224   end
225
226   def services
227     [gitlab_ci_service].compact
228   end
229
230   def gitlab_ci?
231     gitlab_ci_service && gitlab_ci_service.active
232   end
233
234   # For compatibility with old code
235   def code
236     path
237   end
238
239   def items_for entity
240     case entity
241     when 'issue' then
242       issues
243     when 'merge_request' then
244       merge_requests
245     end
246   end
247
248   def send_move_instructions
249     self.users_projects.each do |member|
250       Notify.delay.project_was_moved_email(member.id)
251     end
252   end
253
254   def owner
255     if namespace
256       namespace_owner
257     else
258       creator
259     end
260   end
261
262   def team_member_by_name_or_email(name = nil, email = nil)
263     user = users.where("name like ? or email like ?", name, email).first
264     users_projects.where(user: user) if user
265   end
266
267   # Get Team Member record by user id
268   def team_member_by_id(user_id)
269     users_projects.find_by_user_id(user_id)
270   end
271
272   def name_with_namespace
273     @name_with_namespace ||= begin
274                                if namespace
275                                  namespace.human_name + " / " + name
276                                else
277                                  name
278                                end
279                              end
280   end
281
282   def namespace_owner
283     namespace.try(:owner)
284   end
285
286   def path_with_namespace
287     if namespace
288       namespace.path + '/' + path
289     else
290       path
291     end
292   end
293
294   def transfer(new_namespace)
295     ProjectTransferService.new.transfer(self, new_namespace)
296   end
297
298   def execute_hooks(data)
299     hooks.each { |hook| hook.async_execute(data) }
300   end
301
302   def execute_services(data)
303     services.each do |service|
304
305       # Call service hook only if it is active
306       service.execute(data) if service.active
307     end
308   end
309
310   def discover_default_branch
311     # Discover the default branch, but only if it hasn't already been set to
312     # something else
313     if repository && default_branch.nil?
314       update_attributes(default_branch: self.repository.discover_default_branch)
315     end
316   end
317
318   def update_merge_requests(oldrev, newrev, ref, user)
319     return true unless ref =~ /heads/
320     branch_name = ref.gsub("refs/heads/", "")
321     c_ids = self.repository.commits_between(oldrev, newrev).map(&:id)
322
323     # Update code for merge requests
324     mrs = self.merge_requests.opened.by_branch(branch_name).all
325     mrs.each { |merge_request| merge_request.reload_code; merge_request.mark_as_unchecked }
326
327     # Close merge requests
328     mrs = self.merge_requests.opened.where(target_branch: branch_name).all
329     mrs = mrs.select(&:last_commit).select { |mr| c_ids.include?(mr.last_commit.id) }
330     mrs.each { |merge_request| merge_request.merge!(user.id) }
331
332     true
333   end
334
335   def valid_repo?
336     repository.exists?
337   rescue
338     errors.add(:path, "Invalid repository path")
339     false
340   end
341
342   def empty_repo?
343     !repository.exists? || repository.empty?
344   end
345
346   def ensure_satellite_exists
347     self.satellite.create unless self.satellite.exists?
348   end
349
350   def satellite
351     @satellite ||= Gitlab::Satellite::Satellite.new(self)
352   end
353
354   def repo
355     repository.raw
356   end
357
358   def url_to_repo
359     gitlab_shell.url_to_repo(path_with_namespace)
360   end
361
362   def namespace_dir
363     namespace.try(:path) || ''
364   end
365
366   def repo_exists?
367     @repo_exists ||= repository.exists?
368   rescue
369     @repo_exists = false
370   end
371
372   def open_branches
373     all_branches = repository.branches
374
375     if protected_branches.present?
376       all_branches.reject! do |branch|
377         protected_branches_names.include?(branch.name)
378       end
379     end
380
381     all_branches
382   end
383
384   def protected_branches_names
385     @protected_branches_names ||= protected_branches.map(&:name)
386   end
387
388   def root_ref?(branch)
389     repository.root_ref == branch
390   end
391
392   def ssh_url_to_repo
393     url_to_repo
394   end
395
396   def http_url_to_repo
397     http_url = [Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('')
398   end
399
400   def project_access_human(member)
401     project_user_relation = self.users_projects.find_by_user_id(member.id)
402     self.class.access_options.key(project_user_relation.project_access)
403   end
404
405   # Check if current branch name is marked as protected in the system
406   def protected_branch? branch_name
407     protected_branches_names.include?(branch_name)
408   end
409
410   def forked?
411     !(forked_project_link.nil? || forked_project_link.forked_from_project.nil?)
412   end
413 end