project.rb 10.3 KB
Newer Older
gitlabhq's avatar
gitlabhq committed
1
2
3
require "grit"

class Project < ActiveRecord::Base
gitlabhq's avatar
gitlabhq committed
4
5
  belongs_to :owner, :class_name => "User"

6
  has_many :events, :dependent => :destroy
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
7
  has_many :merge_requests, :dependent => :destroy
VSizov's avatar
VSizov committed
8
  has_many :issues, :dependent => :destroy, :order => "position"
gitlabhq's avatar
gitlabhq committed
9
10
11
  has_many :users_projects, :dependent => :destroy
  has_many :users, :through => :users_projects
  has_many :notes, :dependent => :destroy
gitlabhq's avatar
gitlabhq committed
12
  has_many :snippets, :dependent => :destroy
13
  has_many :deploy_keys, :dependent => :destroy, :foreign_key => "project_id", :class_name => "Key"
14
  has_many :web_hooks, :dependent => :destroy
15
  has_many :protected_branches, :dependent => :destroy
Valery Sizov's avatar
Valery Sizov committed
16
  has_many :wikis, :dependent => :destroy
gitlabhq's avatar
gitlabhq committed
17

Aleksei Kvitinskii's avatar
Aleksei Kvitinskii committed
18
19
  acts_as_taggable

gitlabhq's avatar
gitlabhq committed
20
21
22
23
24
25
26
27
  validates :name,
            :uniqueness => true,
            :presence => true,
            :length   => { :within => 0..255 }

  validates :path,
            :uniqueness => true,
            :presence => true,
28
            :format => { :with => /^[a-zA-Z0-9_\-\.]*$/,
29
                         :message => "only letters, digits & '_' '-' '.' allowed" },
gitlabhq's avatar
gitlabhq committed
30
            :length   => { :within => 0..255 }
Nihad Abbasov's avatar
Nihad Abbasov committed
31

gitlabhq's avatar
gitlabhq committed
32
33
34
35
36
37
  validates :description,
            :length   => { :within => 0..2000 }

  validates :code,
            :presence => true,
            :uniqueness => true,
38
            :format => { :with => /^[a-zA-Z0-9_\-\.]*$/,
39
                         :message => "only letters, digits & '_' '-' '.' allowed"  },
gitlabhq's avatar
gitlabhq committed
40
            :length   => { :within => 3..255 }
gitlabhq's avatar
gitlabhq committed
41

gitlabhq's avatar
gitlabhq committed
42
43
44
  validates :owner,
            :presence => true

Valera Sizov's avatar
Valera Sizov committed
45
  validate :check_limit
gitlabhq's avatar
gitlabhq committed
46
47
  validate :repo_name

48
49
  after_destroy :destroy_repository
  after_save :update_repository
gitlabhq's avatar
gitlabhq committed
50

51
  attr_protected :private_flag, :owner_id
gitlabhq's avatar
gitlabhq committed
52
53

  scope :public_only, where(:private_flag => false)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
54
  scope :without_user, lambda { |user|  where("id not in (:ids)", :ids => user.projects.map(&:id) ) }
gitlabhq's avatar
gitlabhq committed
55

56
57
58
  def self.active
    joins(:issues, :notes, :merge_requests).order("issues.created_at, notes.created_at, merge_requests.created_at DESC")
  end
59
60

  def self.access_options
61
    UsersProject.access_roles
62
63
  end

64
65
66
67
68
  def repository
    @repository ||= Repository.new(self)
  end

  delegate :repo,
69
70
    :url_to_repo,
    :path_to_repo,
71
72
    :update_repository,
    :destroy_repository,
73
74
75
76
    :tags,
    :repo_exists?,
    :commit,
    :commits,
77
    :commits_with_refs,
78
79
80
81
    :tree,
    :heads,
    :commits_since,
    :fresh_commits,
82
    :commits_between,
83
84
    :to => :repository, :prefix => nil

gitlabhq's avatar
gitlabhq committed
85
86
87
88
  def to_param
    code
  end

89
90
91
92
  def web_url
    [GIT_HOST['host'], code].join("/")
  end

93
94
  def observe_push(oldrev, newrev, ref, author_key_id)
    data = web_hook_data(oldrev, newrev, ref, author_key_id)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
95
96
97
98
99
100
101
102

    Event.create(
      :project => self,
      :action => Event::Pushed,
      :data => data
    )
  end

103
  def execute_web_hooks(oldrev, newrev, ref, author_key_id)
104
105
106
107
108
    ref_parts = ref.split('/')

    # Return if this is not a push to a branch (e.g. new commits)
    return if ref_parts[1] !~ /heads/ || oldrev == "00000000000000000000000000000000"

109
    data = web_hook_data(oldrev, newrev, ref, author_key_id)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
110

111
112
113
    web_hooks.each { |web_hook| web_hook.execute(data) }
  end

114
115
  def web_hook_data(oldrev, newrev, ref, author_key_id)
    key = Key.find_by_identifier(author_key_id)
116
117
118
119
    data = {
      before: oldrev,
      after: newrev,
      ref: ref,
120
      user_id: key.user.id,
121
      user_name: key.user_name,
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
      repository: {
        name: name,
        url: web_url,
        description: description,
        homepage: web_url,
        private: private?
      },
      commits: []
    }

    commits_between(oldrev, newrev).each do |commit|
      data[:commits] << {
        id: commit.id,
        message: commit.safe_message,
        timestamp: commit.date.xmlschema,
        url: "http://#{GIT_HOST['host']}/#{code}/commits/#{commit.id}",
        author: {
          name: commit.author_name,
          email: commit.author_email
        }
      }
    end

    data
  end

148
149
150
151
152
153
154
155
156
  def open_branches
    if protected_branches.empty?
      self.repo.heads
    else
      pnames = protected_branches.map(&:name)
      self.repo.heads.reject { |h| pnames.include?(h.name) }
    end.sort_by(&:name)
  end

157
158
159
160
161
  def team_member_by_name_or_email(email = nil, name = nil)
    user = users.where("email like ? or name like ?", email, name).first
    users_projects.find_by_user_id(user.id) if user
  end

162
163
164
165
  def team_member_by_id(user_id)
    users_projects.find_by_user_id(user_id)
  end

166
167
168
169
  def fresh_merge_requests(n)
    merge_requests.includes(:project, :author).order("created_at desc").first(n)
  end

gitlabhq's avatar
gitlabhq committed
170
171
172
173
174
175
176
177
  def fresh_issues(n)
    issues.includes(:project, :author).order("created_at desc").first(n)
  end

  def fresh_notes(n)
    notes.inc_author_project.order("created_at desc").first(n)
  end

gitlabhq's avatar
gitlabhq committed
178
  def common_notes
gitlabhq's avatar
gitlabhq committed
179
    notes.where(:noteable_type => ["", nil]).inc_author_project
gitlabhq's avatar
gitlabhq committed
180
181
  end

182
183
  def build_commit_note(commit)
    notes.new(:noteable_id => commit.id, :noteable_type => "Commit")
gitlabhq's avatar
gitlabhq committed
184
  end
Nihad Abbasov's avatar
Nihad Abbasov committed
185

186
  def commit_notes(commit)
187
188
189
190
    notes.where(:noteable_id => commit.id, :noteable_type => "Commit", :line_code => nil)
  end

  def commit_line_notes(commit)
Valery Sizov's avatar
Valery Sizov committed
191
    notes.where(:noteable_id => commit.id, :noteable_type => "Commit").where("line_code is not null")
gitlabhq's avatar
gitlabhq committed
192
  end
Nihad Abbasov's avatar
Nihad Abbasov committed
193

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
194
195
196
197
  def has_commits?
    !!commit
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
198
199
  # Compatible with all access rights
  # Should be rewrited for new access rights
gitlabhq's avatar
gitlabhq committed
200
  def add_access(user, *access)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
201
    access = if access.include?(:admin) 
202
               { :project_access => UsersProject::MASTER } 
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
203
             elsif access.include?(:write)
204
               { :project_access => UsersProject::DEVELOPER } 
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
205
             else
206
               { :project_access => UsersProject::REPORTER } 
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
207
             end
gitlabhq's avatar
gitlabhq committed
208
    opts = { :user => user }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
209
    opts.merge!(access)
gitlabhq's avatar
gitlabhq committed
210
211
212
213
214
215
216
    users_projects.create(opts)
  end

  def reset_access(user)
    users_projects.where(:project_id => self.id, :user_id => user.id).destroy if self.id
  end

217
218
  def repository_readers
    keys = Key.joins({:user => :users_projects}).
219
      where("users_projects.project_id = ? AND users_projects.project_access = ?", id, UsersProject::REPORTER)
220
    keys.map(&:identifier) + deploy_keys.map(&:identifier)
gitlabhq's avatar
gitlabhq committed
221
222
  end

223
  def repository_writers
224
    keys = Key.joins({:user => :users_projects}).
225
      where("users_projects.project_id = ? AND users_projects.project_access = ?", id, UsersProject::DEVELOPER)
gitlabhq's avatar
gitlabhq committed
226
227
228
    keys.map(&:identifier)
  end

229
230
  def repository_masters
    keys = Key.joins({:user => :users_projects}).
231
      where("users_projects.project_id = ? AND users_projects.project_access = ?", id, UsersProject::MASTER)
232
233
234
    keys.map(&:identifier)
  end

gitlabhq's avatar
gitlabhq committed
235
  def readers
236
    @readers ||= users_projects.includes(:user).map(&:user)
237
238
239
  end

  def writers
240
    @writers ||= users_projects.includes(:user).map(&:user)
gitlabhq's avatar
gitlabhq committed
241
242
243
  end

  def admins
244
    @admins ||= users_projects.includes(:user).where(:project_access => UsersProject::MASTER).map(&:user)
gitlabhq's avatar
gitlabhq committed
245
246
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
247
  def allow_read_for?(user)
248
    !users_projects.where(:user_id => user.id).empty?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
249
250
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
251
  def guest_access_for?(user)
252
    !users_projects.where(:user_id => user.id).empty?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
253
254
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
255
256
  def report_access_for?(user)
    !users_projects.where(:user_id => user.id, :project_access => [UsersProject::REPORTER, UsersProject::DEVELOPER, UsersProject::MASTER]).empty?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
257
258
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
259
260
261
262
263
264
  def dev_access_for?(user)
    !users_projects.where(:user_id => user.id, :project_access => [UsersProject::DEVELOPER, UsersProject::MASTER]).empty?
  end

  def master_access_for?(user)
    !users_projects.where(:user_id => user.id, :project_access => [UsersProject::MASTER]).empty? || owner_id == user.id
265
266
  end

gitlabhq's avatar
gitlabhq committed
267
  def root_ref 
268
    default_branch || "master"
gitlabhq's avatar
gitlabhq committed
269
270
  end

gitlabhq's avatar
gitlabhq committed
271
272
273
274
275
276
277
278
  def public?
    !private_flag
  end

  def private?
    private_flag
  end

279
  def last_activity
gitlabhq's avatar
gitlabhq committed
280
    updates(1).first
281
  rescue
gitlabhq's avatar
gitlabhq committed
282
283
284
285
286
287
288
    nil
  end

  def last_activity_date
    last_activity.try(:created_at)
  end

289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
  def last_activity_date_cached(expire = 1.hour)
    activity_date_key = "project_#{id}_activity_date"

    cached_activities = Rails.cache.read(activity_date_key)
    if cached_activities
      activity_date = if cached_activities == "Never"
                        nil
                      else
                        cached_activities
                      end
    else
      activity_date = last_activity_date
      Rails.cache.write(activity_date_key, activity_date || "Never", :expires_in => expire)
    end

    activity_date
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
307
308
309
310
311
312
313
314
315
  # Get project updates from cache
  # or calculate. 
  def cached_updates(limit, expire = 2.minutes)
    activities_key = "project_#{id}_activities"
    cached_activities = Rails.cache.read(activities_key)
    if cached_activities
      activities = cached_activities
    else
      activities = updates(limit)
316
      Rails.cache.write(activities_key, activities, :expires_in => expire)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
317
318
319
320
321
322
323
    end

    activities
  end

  # Get 20 events for project like
  # commits, issues or notes
gitlabhq's avatar
gitlabhq committed
324
  def updates(n = 3)
325
    [
gitlabhq's avatar
gitlabhq committed
326
      fresh_commits(n),
gitlabhq's avatar
gitlabhq committed
327
328
      fresh_issues(n),
      fresh_notes(n)
gitlabhq's avatar
gitlabhq committed
329
    ].compact.flatten.sort do |x, y|
330
331
332
333
      y.created_at <=> x.created_at
    end[0...n]
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
334
  def activities(n=3)
335
336
337
    [
      fresh_issues(n),
      fresh_merge_requests(n),
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
338
      notes.inc_author_project.where("noteable_type is not null").order("created_at desc").first(n)
339
    ].compact.flatten.sort do |x, y|
gitlabhq's avatar
gitlabhq committed
340
      y.created_at <=> x.created_at
gitlabhq's avatar
gitlabhq committed
341
    end[0...n]
gitlabhq's avatar
gitlabhq committed
342
343
  end

Valera Sizov's avatar
Valera Sizov committed
344
345
  def check_limit
    unless owner.can_create_project?
gitlabhq's avatar
gitlabhq committed
346
      errors[:base] << ("Your own projects limit is #{owner.projects_limit}! Please contact administrator to increase it")
Valera Sizov's avatar
Valera Sizov committed
347
    end
Nihad Abbasov's avatar
Nihad Abbasov committed
348
  rescue
gitlabhq's avatar
gitlabhq committed
349
    errors[:base] << ("Cant check your ability to create project")
Valera Sizov's avatar
Valera Sizov committed
350
351
  end

gitlabhq's avatar
gitlabhq committed
352
  def repo_name
353
354
    if path == "gitolite-admin"
      errors.add(:path, " like 'gitolite-admin' is not allowed")
gitlabhq's avatar
gitlabhq committed
355
356
357
    end
  end

gitlabhq's avatar
gitlabhq committed
358
359
360
361
362
363
364
365
366
367
368
  def valid_repo?
    repo
  rescue
    errors.add(:path, "Invalid repository path")
    false
  end
end
# == Schema Information
#
# Table name: projects
#
369
370
371
372
373
374
375
376
377
378
379
380
381
#  id                     :integer         not null, primary key
#  name                   :string(255)
#  path                   :string(255)
#  description            :text
#  created_at             :datetime
#  updated_at             :datetime
#  private_flag           :boolean         default(TRUE), not null
#  code                   :string(255)
#  owner_id               :integer
#  default_branch         :string(255)     default("master"), not null
#  issues_enabled         :boolean         default(TRUE), not null
#  wall_enabled           :boolean         default(TRUE), not null
#  merge_requests_enabled :boolean         default(TRUE), not null
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
382
#  wiki_enabled           :boolean         default(TRUE), not null
gitlabhq's avatar
gitlabhq committed
383
384
#