Commit 4b4b80fe5f42bfcc4f48c0b054e9a632389ae38b

Refactored model directory
Fixed project list issue

Commit diff

README-APPCAST.txt

 
0
1Gitlab (GPL licensed same as Gitorious)
223rd March 2008
3
4Gitlab is a team repo with a blog, ticketing, forum, wiki etc. that I'm
5merging in to Gitorious - basically a site for a team to host a set of
6private/public projects and manage them. It is for a known set of projects
7but I'll try to make it as general as possible. It's quite a tangent from
8Gitorious so I don't expect any of it to get merged up.
9
10It was simpler to merge Gitlab in than rip the Git code I needed out of
11Gitorious (I found the project via Grit).
12
13Initally I'm merging in without removing (if possible) Gitorious code, there
14is an application_mode set in the config yml that can be tested, if it's
15MODE_GITLAB then the rendering changes etc., setting MODE_GITORIOUS should
16render the existing Gitorious app. I've mainly done this so I can (hopefully)
17pull future Gitorious changes safely.
18
19Notes:
20
21- model and views with fixtures will be first stage, controllers and other
22 logic will be added when m/v finalised, some fixtures included for
23 development
24
25- the style for Gitlab has only been tested on FF2
26
27==
28mtkd (mtkd@prj2.com)
29
30
31
toggle raw diff

README-GITLAB.txt

 
1
2Gitlab (GPL licensed same as Gitorious)
323rd March 2008
4
5Gitlab is a team repo with a blog, ticketing, forum, wiki etc. that I'm
6merging in to Gitorious - basically a site for a team to host a set of
7private/public projects and manage them. It is for a known set of projects
8but I'll try to make it as general as possible. It's quite a tangent from
9Gitorious so I don't expect any of it to get merged up.
10
11It was simpler to merge Gitlab in than rip the Git code I needed out of
12Gitorious (I found the project via Grit).
13
14Initally I'm merging in without removing (if possible) Gitorious code, there
15is an application_mode set in the config yml that can be tested, if it's
16MODE_GITLAB then the rendering changes etc., setting MODE_GITORIOUS should
17render the existing Gitorious app. I've mainly done this so I can (hopefully)
18pull future Gitorious changes safely.
19
20Notes:
21
22- model and views with fixtures will be first stage, controllers and other
23 logic will be added when m/v finalised, some fixtures included for
24 development
25
26- the style for Gitlab has only been tested on FF2
27
28==
29mtkd (mtkd@prj2.com)
30
31
32
toggle raw diff

app/controllers/projects_controller.rb

 
1818 when MODE_GITLAB
1919 @title = "Projects"
2020 @public_projects = Project.find_all_active_public_projects(current_user)
21 flash[:error] = "No public projects exist." if @public_projects.size == 0
22 @member_projects = Project.find_all_active_member_projects(current_user.id) if logged_in?
21 #flash[:error] = "No public projects exist." if @public_projects.size == 0
22 @member_projects = Project.find_all_active_member_projects(current_user) if logged_in?
2323 render :action => "ac_list"
2424 end
2525 end
toggle raw diff

app/helpers/projects_helper.rb

 
5454 out << project_tab(active_tab, PRJ_TAB_WIKI)
5555 out << project_tab(active_tab, PRJ_TAB_EVENTS)
5656 out << project_tab(active_tab, PRJ_TAB_REPOS)
57 out << project_tab(active_tab, PRJ_TAB_MEMBERS)
5758 out << project_tab(active_tab, PRJ_TAB_ROADMAP)
5859 out << project_tab(active_tab, PRJ_TAB_TICKETS)
5960 out << project_tab(active_tab, PRJ_TAB_ARTICLES)
100100 end
101101 out << render_right_project_extrainfo
102102 out << render_right_project_contributors
103 when PRJ_TAB_ACTIVITY
103 when PRJ_TAB_MEMBERS
104104 out << actionbox_basic(:allow_shortcuts => true)
105105 when PRJ_TAB_REPOS
106106 case subtab
toggle raw diff

app/models/article.rb

 
1class Article < ActiveRecord::Base
2
3 belongs_to :user
4 belongs_to :category
5 belongs_to :project
6
7 has_many :comments
8
9 #
10 validates_columns :article_type, :status, :comment_status, :audience
11 validates_presence_of :title, :permalink, :category_id, :user_id
12 validates_length_of :title, :within => 2..128
13 validates_length_of :permalink, :within => 2..128
14 validates_uniqueness_of :title, :permalink, :case_sensitive => false
15
16 # must pass the enum as string
17 def self.format_article_type (at)
18 case at
19 when "release"
20 return "Releases"
21 when "issue"
22 return "Issues"
23 when "howto"
24 return "How To" #TODO this is spare now
25 when "other"
26 return "Others"
27 else
28 return at.to_s.capitalize
29 end
30 end
31
32protected
33
34
35 #FIXME it's possible this could be returning too many results
36 def self.time_delta(year, month = nil, day = nil)
37 from = Time.mktime(year, month || 1, day || 1)
38 to = from + 1.year
39 to = from + 1.month unless month.blank?
40 to = from + 1.day unless day.blank?
41 to = to.tomorrow unless month.blank?
42 return [from, to]
43 end
44
45 #FIXME Audience Issue
46 #there is some conflict between the security system using project audience and articles
47 #poss solution is either to do enforce that articles can't be public if they belong to a private project
48 #or to do the security tests below by joining project and testing project.audience as well as testing
49 #article.audience
50
51
52 #public version of .count to filter articles you are not authorised to see
53 def self.public_count (curruser)
54 @projects = Project.list_for_user(curruser)
55 #FIXME is there a faster way of doing this?
56 @articles = find(:all, :conditions => ["status = '#{:published}' AND ( audience = '#{:public}' OR project_id IN (" << @projects << ") )"], :order => "created_at DESC")
57 return @articles.size
58 end
59
60 #
61 # finds articles for \2008 or \2008\11 or \2008\11\1
62 #
63 def self.find_by_date (curruser, year, month = nil, day = nil)
64 from, to = self.time_delta(year, month, day)
65 @projects = Project.list_for_user(curruser)
66 find(:all, :conditions => [ "status = '#{:published}' AND (created_at BETWEEN ? AND ?) AND ( audience = '#{:public}' OR project_id IN (" << @projects << ") )", from, to ])
67 end
68
69 #
70 def self.find_all_articles (curruser)
71 @projects = Project.list_for_user(curruser)
72 find(:all, :conditions => ["status = '#{:published}' AND ( audience = '#{:public}' OR project_id IN (" << @projects << ") )"], :order => "created_at DESC")
73 end
74
75 #
76 def self.find_home (curruser)
77 @projects = Project.list_for_user(curruser)
78 find(:all, :conditions => ["status = '#{:published}' AND ( audience = '#{:public}' OR project_id IN (" << @projects << ") )"], :order => "created_at DESC", :limit => 3)
79 end
80
81 # finds articles for a user based on the user slug e.g. /profiles/test-user/articles
82 def self.find_by_user (user_id, curruser)
83 @projects = Project.list_for_user(curruser)
84 find(:all, :conditions => ["status = '#{:published}' AND user_id = '#{user_id}' AND ( audience = '#{:public}' OR project_id IN (" << @projects << ") )"], :order => "created_at DESC")
85 end
86
87 # finds articles for a user/project based on the user slug e.g. /profiles/test-user/project/project_id/articles
88 def self.find_by_user_project (user_id, project_id, curruser)
89 @projects = Project.list_for_user(curruser)
90 find(:all, :conditions => ["status = '#{:published}' AND user_id = '#{user_id}' AND project_id = '#{project_id}' AND ( audience = '#{:public}' OR project_id IN (" << @projects << ") )"], :order => "created_at DESC")
91 end
92
93 #
94 def self.find_by_permalink (permalink, curruser)
95 @projects = Project.list_for_user(curruser)
96 find(:first, :conditions => ["status = '#{:published}' AND permalink = '#{permalink}' AND ( audience = '#{:public}' OR project_id IN (" << @projects << ") )"])
97 end
98
99 #
100 def self.find_by_type (article_type, curruser)
101 @projects = Project.list_for_user(curruser)
102 find(:all, :conditions => ["status = '#{:published}' AND article_type = '#{article_type}' AND ( audience = '#{:public}' OR project_id IN (" << @projects << ") )"], :order => "created_at DESC")
103 end
104
105 #
106 def self.find_by_category (category_id, curruser)
107 @projects = Project.list_for_user(curruser)
108 find(:all, :conditions => ["status = '#{:published}' AND category_id = '#{category_id}' AND ( audience = '#{:public}' OR project_id IN (" << @projects << ") )"], :order => "created_at DESC")
109 end
110
111 #
112 def self.find_by_sitename
113 find(:all, :conditions => ["status = '#{:published}' AND project_id = '1'"], :order => "created_at DESC")
114 end
115
116 #
117 def self.find_by_project (project_id, curruser)
118 projects_allowed = Project.list_for_user(curruser)
119 find(:all, :conditions => ["status = '#{:published}' AND project_id = '#{project_id}' AND ( audience = '#{:public}' OR project_id IN (" << projects_allowed << ") )"], :order => "created_at DESC")
120 end
121
122 #
123 def self.find_by_sitename_and_type (article_type)
124 find(:all, :conditions => ["status = '#{:published}' AND project_id = '1' AND article_type = '#{article_type}'"], :order => "created_at DESC")
125 end
126
127 #
128 def self.find_by_project_and_type (article_type, project_id, curruser)
129 projects_allowed = Project.list_for_user(curruser)
130 find(:all, :conditions => ["status = '#{:published}' AND project_id = '#{project_id}' AND article_type = '#{article_type}' AND ( audience = '#{:public}' OR project_id IN (" << projects_allowed << ") )"], :order => "created_at DESC")
131 end
132
133 #
134 def self.find_sitename_type_counts
135 find_by_sql ["SELECT article_types.article_type, IFNULL(COUNT(articles.id),0) AS type_count
136 FROM article_types
137 LEFT JOIN articles ON article_types.article_type = articles.article_type AND (articles.project_id = 1 AND articles.status = '#{:published}')
138 GROUP BY article_types.article_type
139 ORDER BY article_types.id"]
140 end
141
142 #
143 def self.find_project_type_counts (project_id)
144 find_by_sql ["SELECT article_types.article_type, IFNULL(COUNT(articles.id),0) AS type_count
145 FROM article_types
146 LEFT JOIN articles ON article_types.article_type = articles.article_type AND (articles.project_id = '#{project_id}' AND articles.status = '#{:published}')
147 GROUP BY article_types.article_type
148 ORDER BY article_types.id"]
149 end
150
151 # only show public articles on the main feed
152 # FIXME do a seperate feed on the private user page with project member articles included
153 def self.find_rss
154 find(:all, :conditions => ["status = '#{:published}' AND audience = '#{:public}'"], :order => "created_at DESC", :limit => 10)
155 end
156
157 #
158 def self.admin_find_all_articles
159 find(:all, :order => "created_at DESC")
160 end
161
162 # finds articles for a user based on the user slug e.g. /profiles/test-user/articles
163 def self.admin_find_by_user (user_id)
164 find(:all, :conditions => ["user_id = '#{user_id}'"], :order => "created_at DESC")
165 end
166
167 #
168 def self.admin_find_by_category(category_id)
169 find(:all, :conditions => ["category_id = '#{category_id}'"], :order => "created_at DESC")
170 end
171
172 #
173 def self.admin_find_by_search(phrase)
174 find(:all, :conditions => ["title like ?", "%" << phrase << "%"], :order => "created_at DESC")
175 end
176
177end
178
179
180
181
182
183
184
185
186
187
toggle raw diff

app/models/category.rb

 
1class Category < ActiveRecord::Base
2
3 belongs_to :user
4
5 has_many :articles
6
7
8 protected
9
10 #
11 def self.find_by_all
12 find(:all, :order => 'parent ASC, id ASC')
13 end
14
15 #
16 def self.find_by_all_with_count (curruser)
17 @projects = Project.list_for_user(curruser)
18 find_by_sql ["SELECT categories.name, category_type, categories.slug, COUNT(articles.id) AS num_articles FROM categories JOIN articles ON categories.id = articles.category_id WHERE articles.status = '#{:published}' AND (articles.audience = '#{:public}' OR articles.project_id IN (" << @projects << ") ) GROUP BY articles.category_id ORDER BY parent ASC, categories.name"]
19 end
20
21 #
22 def self.find_by_slug(slug)
23 find(:first, :conditions => ["slug = '#{slug}'"])
24 end
25
26 #
27 def self.find_by_search(phrase)
28 #find(:all, :conditions => ["status = '#{:published}' AND title like ?", "%" << phrase << "%"])
29 end
30
31
32end
toggle raw diff

app/models/committership.rb

 
0class Committership < ActiveRecord::Base
1 belongs_to :user
2 belongs_to :repository
3
4 KIND_ACCESS_NONE = 0
5 KIND_ACCESS_READ = 1
6 KIND_ACCESS_WRITE = 2
7end
toggle raw diff

app/models/event/event.rb

 
1class Event < ActiveRecord::Base
2
3 protected
4
5 def self.find_by_project(project_id)
6 find(:all, :conditions => ["project_id = '#{project_id}'"], :order => "created_at DESC")
7 end
8
9end
toggle raw diff

app/models/git/committership.rb

 
1class Committership < ActiveRecord::Base
2 belongs_to :user
3 belongs_to :repository
4
5 KIND_ACCESS_NONE = 0
6 KIND_ACCESS_READ = 1
7 KIND_ACCESS_WRITE = 2
8end
toggle raw diff

app/models/git/merge_request.rb

 
1class MergeRequest < ActiveRecord::Base
2 belongs_to :user
3 belongs_to :source_repository, :class_name => 'Repository'
4 belongs_to :target_repository, :class_name => 'Repository'
5
6 is_indexed :fields => ["proposal"], :include => [{
7 :association_name => "user",
8 :field => "login",
9 :as => "proposed_by"
10 }], :conditions => "status = 0"
11
12 attr_protected :user_id, :status
13
14 validates_presence_of :user, :source_repository, :target_repository
15
16 STATUS_OPEN = 0
17 STATUS_MERGED = 1
18 STATUS_REJECTED = 2
19
20 def self.statuses
21 { "Open" => STATUS_OPEN, "Merged" => STATUS_MERGED, "Rejected" => STATUS_REJECTED }
22 end
23
24 def self.count_open
25 count(:all, :conditions => {:status => STATUS_OPEN})
26 end
27
28 def status_string
29 self.class.statuses.invert[status].downcase
30 end
31
32 def open?
33 status == STATUS_OPEN
34 end
35
36 def merged?
37 status == STATUS_MERGED
38 end
39
40 def rejected?
41 status == STATUS_REJECTED
42 end
43
44 def source_branch
45 super || "master"
46 end
47
48 def target_branch
49 super || "master"
50 end
51
52 def source_name
53 if source_repository
54 "#{source_repository.name}:#{source_branch}"
55 end
56 end
57
58 def target_name
59 if target_repository
60 "#{target_repository.name}:#{target_branch}"
61 end
62 end
63
64 def resolvable_by?(candidate)
65 candidate == target_repository.user
66 end
67end
toggle raw diff

app/models/git/merge_request_observer.rb

 
1class MergeRequestObserver < ActiveRecord::Observer
2
3 def after_create(record)
4 Mailer.deliver_merge_request_notification(record)
5 end
6
7end
toggle raw diff

app/models/git/repository.rb

 
1class Repository < ActiveRecord::Base
2 belongs_to :user
3 belongs_to :project
4 belongs_to :parent, :class_name => "Repository"
5 has_many :committerships, :dependent => :destroy
6 has_many :committers, :through => :committerships, :source => :user
7 has_many :comments, :dependent => :destroy
8 has_many :merge_requests, :foreign_key => 'target_repository_id',
9 :order => "status, id desc", :dependent => :destroy
10 has_many :proposed_merge_requests, :foreign_key => 'source_repository_id',
11 :class_name => 'MergeRequest', :order => "id desc", :dependent => :destroy
12
13 validates_presence_of :user_id, :project_id, :name
14 validates_format_of :name, :with => /^[a-z0-9_\-]+$/i,
15 :message => "is invalid, must match something like /[a-z0-9_\\-]+/"
16 validates_uniqueness_of :name, :scope => :project_id, :case_sensitive => false
17
18 before_save :set_as_mainline_if_first
19 after_create :add_user_as_committer, :create_new_repos_task
20 after_destroy :create_delete_repos_task
21
22 def self.new_by_cloning(other, username=nil)
23 suggested_name = username ? "#{username}s-clone" : nil
24 new(:parent => other, :project => other.project, :name => suggested_name)
25 end
26
27 def self.find_by_name!(name)
28 find_by_name(name) || raise(ActiveRecord::RecordNotFound)
29 end
30
31 def self.create_git_repository(path)
32 git_backend.create(full_path_from_partial_path(path))
33 end
34
35 def self.clone_git_repository(target_path, source_path)
36 git_backend.clone(full_path_from_partial_path(target_path),
37 full_path_from_partial_path(source_path))
38 end
39
40 def self.delete_git_repository(path)
41 git_backend.delete!(full_path_from_partial_path(path))
42 end
43
44 def gitdir
45 File.join(project.slug, "#{name}.git")
46 end
47
48 def clone_url
49 "git://#{GitoriousConfig['gitorious_host']}/#{gitdir}"
50 end
51
52 def http_clone_url
53 "http://git.#{GitoriousConfig['gitorious_host']}/#{gitdir}"
54 end
55
56 def push_url
57 "git@#{GitoriousConfig['gitorious_host']}:#{gitdir}"
58 end
59
60 def full_repository_path
61 self.class.full_path_from_partial_path(gitdir)
62 end
63
64 def git
65 Grit::Repo.new(full_repository_path)
66 end
67
68 def has_commits?
69 return false if new_record? || !ready?
70 !git.heads.empty?
71 end
72
73 def self.git_backend
74 RAILS_ENV == "test" ? MockGitBackend : GitBackend
75 end
76
77 def git_backend
78 RAILS_ENV == "test" ? MockGitBackend : GitBackend
79 end
80
81 def to_param
82 name
83 end
84
85 def to_xml(opts = {})
86 super({:methods => [:gitdir, :clone_url, :push_url]}.merge(opts))
87 end
88
89 def add_committer(user)
90 unless user.can_write_to?(self)
91 committers << user
92 end
93 end
94
95 def head_candidate
96 return nil unless has_commits?
97 @head_candidate ||= git.heads.find{|h| h.name == "master"} || git.heads.first
98 end
99
100 def last_commit
101 if has_commits?
102 @last_commit ||= git.commits(head_candidate.name, 1).first
103 end
104 @last_commit
105 end
106
107 def can_be_deleted_by?(candidate)
108 !mainline? && (candidate == user)
109 end
110
111 def create_new_repos_task
112 Task.create!(:target_class => self.class.name,
113 :command => parent ? "clone_git_repository" : "create_git_repository",
114 :arguments => parent ? [gitdir, parent.gitdir] : [gitdir],
115 :target_id => self.id)
116 end
117
118 def create_delete_repos_task
119 Task.create!(:target_class => self.class.name,
120 :command => "delete_git_repository", :arguments => [gitdir])
121 end
122
123 def paginated_commits(tree_name, page, per_page = 30)
124 per_page = GitoriousConfig["number_of_displayed_commits"]
125 page = (page || 1).to_i
126 total = git.commit_count(tree_name)
127 offset = (page - 1) * per_page
128 commits = WillPaginate::Collection.new(page, per_page, total)
129 commits.replace git.commits(tree_name, per_page, offset)
130 end
131
132
133 def count_commits_from_last_week_by_user(user)
134 return 0 unless has_commits?
135
136 commits_by_email = git.commits_since("master", "last week").collect do |commit|
137 commit.committer.email == user.email
138 end
139 commits_by_email.size
140 end
141
142 # TODO: cache
143 def commit_graph_data(head = "master")
144 commits = git.commits_since(head, "24 weeks ago")
145 commits_by_week = commits.group_by{|c| c.committed_date.strftime("%W") }
146
147 # build an initial empty set of 24 week commit data
148 weeks = [1.day.from_now-1.week]
149 23.times{|w| weeks << weeks.last-1.week }
150 week_numbers = weeks.map{|d| d.strftime("%W") }
151 commits = (0...24).to_a.map{|i| 0 }
152
153 commits_by_week.each do |week, commits_in_week|
154 if week_pos = week_numbers.index(week)
155 commits[week_pos+1] = commits_in_week.size
156 end
157 end
158 commits = [] if commits.max == 0
159 [week_numbers.reverse, commits.reverse]
160 end
161
162 # TODO: refactor into simpler approach
163 # TODO: caching
164 def commit_graph_data_by_author(head = "master")
165 h = Hash.new
166
167 data = self.git.git.rev_list({:pretty => "format:name:%cn", :since => "1 years ago" }, head)
168 data.each_line do |line|
169 if line =~ /^name:(.*)$/ then
170 author = $1
171
172 if h[author]