project.rb 10.3 KB
Newer Older
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets committed
1
2
3
4
5
require "grit"

class Project < ActiveRecord::Base
  belongs_to :owner, :class_name => "User"

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
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"
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets 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
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets committed
17

Aleksei Kvitinskii's avatar
Aleksei Kvitinskii committed
18
19
  acts_as_taggable

Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets 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" },
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets committed
30
            :length   => { :within => 0..255 }
Nihad Abbasov's avatar
Nihad Abbasov committed
31

Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets 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 }
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets committed
41
42
43
44
45

  validates :owner,
            :presence => true

  validate :check_limit
gitlabhq's avatar
gitlabhq committed
46
47
  validate :repo_name

48
49
  after_destroy :destroy_repository
  after_save :update_repository
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets committed
50
51
52
53

  attr_protected :private_flag, :owner_id

  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) ) }
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets 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

Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets 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

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

182
183
  def build_commit_note(commit)
    notes.new(:noteable_id => commit.id, :noteable_type => "Commit")
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets 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")
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets 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
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets 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
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets committed
208
    opts = { :user => user }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
209
    opts.merge!(access)
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets 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
  def repository_readers
218
    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)
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets 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)
226
    keys.map(&:identifier)
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets committed
227
228
  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

Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets 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)
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets committed
241
242
243
  end

  def admins
244
    @admins ||= users_projects.includes(:user).where(:project_access => UsersProject::MASTER).map(&:user)
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets 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

Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets 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
330
    ].compact.flatten.sort do |x, y|
      y.created_at <=> x.created_at
gitlabhq's avatar
gitlabhq committed
331
    end[0...n]
gitlabhq's avatar
gitlabhq committed
332
333
  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
340
341
342
343
    ].compact.flatten.sort do |x, y|
      y.created_at <=> x.created_at
    end[0...n]
  end

Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets committed
344
345
346
347
  def check_limit
    unless owner.can_create_project?
      errors[:base] << ("Your own projects limit is #{owner.projects_limit}! Please contact administrator to increase it")
    end
Nihad Abbasov's avatar
Nihad Abbasov committed
348
  rescue
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets committed
349
350
351
    errors[:base] << ("Cant check your ability to create project")
  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

Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets 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
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets committed
383
384
#