project.rb 10.7 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
7
  has_many :users,          :through => :users_projects
  has_many :events,         :dependent => :destroy
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
8
  has_many :merge_requests, :dependent => :destroy
9
  has_many :issues,         :dependent => :destroy, :order => "position"
gitlabhq's avatar
gitlabhq committed
10
  has_many :users_projects, :dependent => :destroy
11
12
13
14
15
  has_many :notes,          :dependent => :destroy
  has_many :snippets,       :dependent => :destroy
  has_many :deploy_keys,    :dependent => :destroy, :foreign_key => "project_id", :class_name => "Key"
  has_many :web_hooks,      :dependent => :destroy
  has_many :wikis,          :dependent => :destroy
16
  has_many :protected_branches, :dependent => :destroy
Aleksei Kvitinskii's avatar
Aleksei Kvitinskii committed
17

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

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

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

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

40
  validates :owner, :presence => true
Valera Sizov's avatar
Valera Sizov committed
41
  validate :check_limit
gitlabhq's avatar
gitlabhq committed
42
43
  validate :repo_name

44
  attr_protected :private_flag, :owner_id
gitlabhq's avatar
gitlabhq committed
45
46

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

49
50
51
  def self.active
    joins(:issues, :notes, :merge_requests).order("issues.created_at, notes.created_at, merge_requests.created_at DESC")
  end
52
53

  def self.access_options
54
    UsersProject.access_roles
55
56
  end

57
58
59
60
  def self.search query
    where("name like :query or code like :query or path like :query", :query => "%#{query}%")
  end

gitlabhq's avatar
gitlabhq committed
61
62
63
64
  def to_param
    code
  end

65
66
67
68
  def web_url
    [GIT_HOST['host'], code].join("/")
  end

69
70
  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
71
72
73
74

    Event.create(
      :project => self,
      :action => Event::Pushed,
75
      :data => data,
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
76
      :author_id => data[:user_id]
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
77
78
79
    )
  end

80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
  def update_merge_requests(oldrev, newrev, ref, author_key_id)
    return true unless ref =~ /heads/
    branch_name = ref.gsub("refs/heads/", "")

    key = Key.find_by_identifier(author_key_id)
    user = key.user

    c_ids = self.commits_between(oldrev, newrev).map(&:id)

    # update commits & diffs for existing MR
    mrs = self.merge_requests.opened.where(:source_branch => branch_name).all
    mrs.each do |merge_request| 
      merge_request.reloaded_commits
      merge_request.reloaded_diffs
    end

    # Close merge requests
    mrs = self.merge_requests.opened.where(:target_branch => branch_name).all
    mrs.each do |merge_request| 
      next unless merge_request.last_commit
      # Mark as merged & create event if merged
      if c_ids.include?(merge_request.last_commit.id)
        merge_request.mark_as_merged!
        Event.create(
          :project => self,
          :action => Event::Merged,
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
106
107
          :target_id => merge_request.id,
          :target_type => "MergeRequest",
108
109
110
111
112
113
114
          :author_id => user.id
        )
      end
    end
    true
  end

115
  def execute_web_hooks(oldrev, newrev, ref, author_key_id)
116
117
118
119
120
    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"

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

123
124
125
    web_hooks.each { |web_hook| web_hook.execute(data) }
  end

126
127
  def web_hook_data(oldrev, newrev, ref, author_key_id)
    key = Key.find_by_identifier(author_key_id)
128
129
130
131
    data = {
      before: oldrev,
      after: newrev,
      ref: ref,
132
      user_id: key.user.id,
133
      user_name: key.user_name,
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
      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

160
161
162
163
164
165
166
167
168
  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

169
170
171
172
173
  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

174
175
176
177
  def team_member_by_id(user_id)
    users_projects.find_by_user_id(user_id)
  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

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
235
  def allow_read_for?(user)
236
    !users_projects.where(:user_id => user.id).empty?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
237
238
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
239
  def guest_access_for?(user)
240
    !users_projects.where(:user_id => user.id).empty?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
241
242
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
243
244
  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
245
246
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
247
248
249
250
251
252
  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
253
254
  end

gitlabhq's avatar
gitlabhq committed
255
  def root_ref 
256
    default_branch || "master"
gitlabhq's avatar
gitlabhq committed
257
258
  end

gitlabhq's avatar
gitlabhq committed
259
260
261
262
263
264
265
266
  def public?
    !private_flag
  end

  def private?
    private_flag
  end

267
  def last_activity
268
    events.last || nil
gitlabhq's avatar
gitlabhq committed
269
270
271
  end

  def last_activity_date
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
272
273
    if events.last
      events.last.created_at
274
    else
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
275
      updated_at
276
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
277
  end
278

Valera Sizov's avatar
Valera Sizov committed
279
280
  def check_limit
    unless owner.can_create_project?
gitlabhq's avatar
gitlabhq committed
281
      errors[:base] << ("Your own projects limit is #{owner.projects_limit}! Please contact administrator to increase it")
Valera Sizov's avatar
Valera Sizov committed
282
    end
Nihad Abbasov's avatar
Nihad Abbasov committed
283
  rescue
gitlabhq's avatar
gitlabhq committed
284
    errors[:base] << ("Cant check your ability to create project")
Valera Sizov's avatar
Valera Sizov committed
285
286
  end

gitlabhq's avatar
gitlabhq committed
287
  def repo_name
288
289
    if path == "gitolite-admin"
      errors.add(:path, " like 'gitolite-admin' is not allowed")
gitlabhq's avatar
gitlabhq committed
290
291
292
    end
  end

gitlabhq's avatar
gitlabhq committed
293
294
295
296
297
298
  def valid_repo?
    repo
  rescue
    errors.add(:path, "Invalid repository path")
    false
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
299
300

  def commit(commit_id = nil)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
301
    Commit.find_or_first(repo, commit_id)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
302
303
304
  end

  def fresh_commits(n = 10)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
305
    Commit.fresh_commits(repo, n)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
306
307
308
  end

  def commits_with_refs(n = 20)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
309
    Commit.commits_with_refs(repo, n)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
310
311
312
  end

  def commits_since(date)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
313
    Commit.commits_since(repo, date)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
314
315
316
  end

  def commits(ref, path = nil, limit = nil, offset = nil)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
317
    Commit.commits(repo, ref, path, limit, offset)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
318
319
320
  end

  def commits_between(from, to)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
321
    Commit.commits_between(repo, from, to)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
  end

  def project_id
    self.id
  end

  def write_hooks
    %w(post-receive).each do |hook|
      write_hook(hook, File.read(File.join(Rails.root, 'lib', "#{hook}-hook")))
    end
  end

  def write_hook(name, content)
    hook_file = File.join(path_to_repo, 'hooks', name)

    File.open(hook_file, 'w') do |f|
      f.write(content)
    end

    File.chmod(0775, hook_file)
  end

  def repo
    @repo ||= Grit::Repo.new(path_to_repo)
  end

  def url_to_repo
    Gitlabhq::GitHost.url_to_repo(path)
  end

  def path_to_repo
    File.join(GIT_HOST["base_path"], "#{path}.git")
  end

  def update_repository
    Gitlabhq::GitHost.system.update_project(path, self)

    write_hooks if File.exists?(path_to_repo)
  end

  def destroy_repository
    Gitlabhq::GitHost.system.destroy_project(self)
  end

  def repo_exists?
    @repo_exists ||= (repo && !repo.branches.empty?)
  rescue 
    @repo_exists = false
  end

  def tags
    repo.tags.map(&:name).sort.reverse
  end

  def heads
    @heads ||= repo.heads
  end

  def tree(fcommit, path = nil)
    fcommit = commit if fcommit == :head
    tree = fcommit.tree
    path ? (tree / path) : tree
  end
gitlabhq's avatar
gitlabhq committed
385
end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
386

gitlabhq's avatar
gitlabhq committed
387
388
389
390
# == Schema Information
#
# Table name: projects
#
391
392
393
394
395
396
397
398
399
400
401
402
403
#  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
404
#  wiki_enabled           :boolean         default(TRUE), not null
gitlabhq's avatar
gitlabhq committed
405
406
#