project.rb 10.6 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

gitlabhq's avatar
gitlabhq committed
57
58
59
60
  def to_param
    code
  end

61
62
63
64
  def web_url
    [GIT_HOST['host'], code].join("/")
  end

65
66
  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
67
68
69
70

    Event.create(
      :project => self,
      :action => Event::Pushed,
71
      :data => data,
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
72
      :author_id => data[:user_id]
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
73
74
75
    )
  end

76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
  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
102
103
          :target_id => merge_request.id,
          :target_type => "MergeRequest",
104
105
106
107
108
109
110
          :author_id => user.id
        )
      end
    end
    true
  end

111
  def execute_web_hooks(oldrev, newrev, ref, author_key_id)
112
113
114
115
116
    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"

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

119
120
121
    web_hooks.each { |web_hook| web_hook.execute(data) }
  end

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

156
157
158
159
160
161
162
163
164
  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

165
166
167
168
169
  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

170
171
172
173
  def team_member_by_id(user_id)
    users_projects.find_by_user_id(user_id)
  end

gitlabhq's avatar
gitlabhq committed
174
  def common_notes
gitlabhq's avatar
gitlabhq committed
175
    notes.where(:noteable_type => ["", nil]).inc_author_project
gitlabhq's avatar
gitlabhq committed
176
177
  end

178
179
  def build_commit_note(commit)
    notes.new(:noteable_id => commit.id, :noteable_type => "Commit")
gitlabhq's avatar
gitlabhq committed
180
  end
Nihad Abbasov's avatar
Nihad Abbasov committed
181

182
  def commit_notes(commit)
183
184
185
186
    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
187
    notes.where(:noteable_id => commit.id, :noteable_type => "Commit").where("line_code is not null")
gitlabhq's avatar
gitlabhq committed
188
  end
Nihad Abbasov's avatar
Nihad Abbasov committed
189

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
190
191
192
193
  def has_commits?
    !!commit
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
194
195
  # Compatible with all access rights
  # Should be rewrited for new access rights
gitlabhq's avatar
gitlabhq committed
196
  def add_access(user, *access)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
197
    access = if access.include?(:admin) 
198
               { :project_access => UsersProject::MASTER } 
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
199
             elsif access.include?(:write)
200
               { :project_access => UsersProject::DEVELOPER } 
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
201
             else
202
               { :project_access => UsersProject::REPORTER } 
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
203
             end
gitlabhq's avatar
gitlabhq committed
204
    opts = { :user => user }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
205
    opts.merge!(access)
gitlabhq's avatar
gitlabhq committed
206
207
208
209
210
211
212
    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

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

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

225
226
  def repository_masters
    keys = Key.joins({:user => :users_projects}).
227
      where("users_projects.project_id = ? AND users_projects.project_access = ?", id, UsersProject::MASTER)
228
229
230
    keys.map(&:identifier)
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
231
  def allow_read_for?(user)
232
    !users_projects.where(:user_id => user.id).empty?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
233
234
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
235
  def guest_access_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
240
  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
241
242
  end

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

gitlabhq's avatar
gitlabhq committed
251
  def root_ref 
252
    default_branch || "master"
gitlabhq's avatar
gitlabhq committed
253
254
  end

gitlabhq's avatar
gitlabhq committed
255
256
257
258
259
260
261
262
  def public?
    !private_flag
  end

  def private?
    private_flag
  end

263
  def last_activity
264
    events.last || nil
gitlabhq's avatar
gitlabhq committed
265
266
267
  end

  def last_activity_date
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
268
269
    if events.last
      events.last.created_at
270
    else
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
271
      updated_at
272
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
273
  end
274

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

gitlabhq's avatar
gitlabhq committed
283
  def repo_name
284
285
    if path == "gitolite-admin"
      errors.add(:path, " like 'gitolite-admin' is not allowed")
gitlabhq's avatar
gitlabhq committed
286
287
288
    end
  end

gitlabhq's avatar
gitlabhq committed
289
290
291
292
293
294
  def valid_repo?
    repo
  rescue
    errors.add(:path, "Invalid repository path")
    false
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
295
296

  def commit(commit_id = nil)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
297
    Commit.find_or_first(repo, commit_id)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
298
299
300
  end

  def fresh_commits(n = 10)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
301
    Commit.fresh_commits(repo, n)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
302
303
304
  end

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

  def commits_since(date)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
309
    Commit.commits_since(repo, date)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
310
311
312
  end

  def commits(ref, path = nil, limit = nil, offset = nil)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
313
    Commit.commits(repo, ref, path, limit, offset)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
314
315
316
  end

  def commits_between(from, to)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
317
    Commit.commits_between(repo, from, to)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
318
319
320
321
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
  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
381
end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
382

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