summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/ability.rb26
-rw-r--r--app/models/application_setting.rb43
-rw-r--r--app/models/broadcast_message.rb2
-rw-r--r--app/models/commit.rb25
-rw-r--r--app/models/concerns/issuable.rb40
-rw-r--r--app/models/concerns/mentionable.rb18
-rw-r--r--app/models/concerns/sortable.rb35
-rw-r--r--app/models/email.rb2
-rw-r--r--app/models/event.rb115
-rw-r--r--app/models/external_issue.rb25
-rw-r--r--app/models/group.rb39
-rw-r--r--app/models/group_milestone.rb6
-rw-r--r--app/models/hooks/web_hook.rb11
-rw-r--r--app/models/identity.rb16
-rw-r--r--app/models/issue.rb1
-rw-r--r--app/models/key.rb7
-rw-r--r--app/models/label.rb2
-rw-r--r--app/models/member.rb1
-rw-r--r--app/models/members/group_member.rb14
-rw-r--r--app/models/members/project_member.rb23
-rw-r--r--app/models/merge_request.rb27
-rw-r--r--app/models/merge_request_diff.rb2
-rw-r--r--app/models/milestone.rb1
-rw-r--r--app/models/namespace.rb30
-rw-r--r--app/models/network/graph.rb4
-rw-r--r--app/models/note.rb110
-rw-r--r--app/models/notification.rb10
-rw-r--r--app/models/project.rb274
-rw-r--r--app/models/project_contributions.rb23
-rw-r--r--app/models/project_services/asana_service.rb117
-rw-r--r--app/models/project_services/assembla_service.rb3
-rw-r--r--app/models/project_services/bamboo_service.rb33
-rw-r--r--app/models/project_services/buildbox_service.rb4
-rw-r--r--app/models/project_services/campfire_service.rb7
-rw-r--r--app/models/project_services/ci_service.rb3
-rw-r--r--app/models/project_services/custom_issue_tracker_service.rb53
-rw-r--r--app/models/project_services/emails_on_push_service.rb3
-rw-r--r--app/models/project_services/flowdock_service.rb6
-rw-r--r--app/models/project_services/gemnasium_service.rb6
-rw-r--r--app/models/project_services/gitlab_ci_service.rb9
-rw-r--r--app/models/project_services/gitlab_issue_tracker_service.rb46
-rw-r--r--app/models/project_services/hipchat_service.rb21
-rw-r--r--app/models/project_services/issue_tracker_service.rb114
-rw-r--r--app/models/project_services/jira_service.rb53
-rw-r--r--app/models/project_services/pivotaltracker_service.rb3
-rw-r--r--app/models/project_services/pushover_service.rb7
-rw-r--r--app/models/project_services/redmine_service.rb39
-rw-r--r--app/models/project_services/slack_message.rb20
-rw-r--r--app/models/project_services/slack_service.rb3
-rw-r--r--app/models/project_services/teamcity_service.rb134
-rw-r--r--app/models/project_team.rb2
-rw-r--r--app/models/project_wiki.rb4
-rw-r--r--app/models/protected_branch.rb11
-rw-r--r--app/models/repository.rb96
-rw-r--r--app/models/service.rb33
-rw-r--r--app/models/snippet.rb13
-rw-r--r--app/models/user.rb153
-rw-r--r--app/models/wiki_page.rb2
58 files changed, 1523 insertions, 407 deletions
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 97a72bf3635..890417e780d 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -73,28 +73,28 @@ class Ability
# Rules based on role in project
if team.master?(user)
- rules += project_master_rules
+ rules.push(*project_master_rules)
elsif team.developer?(user)
- rules += project_dev_rules
+ rules.push(*project_dev_rules)
elsif team.reporter?(user)
- rules += project_report_rules
+ rules.push(*project_report_rules)
elsif team.guest?(user)
- rules += project_guest_rules
+ rules.push(*project_guest_rules)
end
if project.public? || project.internal?
- rules += public_project_rules
+ rules.push(*public_project_rules)
end
if project.owner == user || user.admin?
- rules += project_admin_rules
+ rules.push(*project_admin_rules)
end
if project.group && project.group.has_owner?(user)
- rules += project_admin_rules
+ rules.push(*project_admin_rules)
end
if project.archived?
@@ -193,17 +193,17 @@ class Ability
# Only group masters and group owners can create new projects in group
if group.has_master?(user) || group.has_owner?(user) || user.admin?
- rules += [
+ rules.push(*[
:create_projects,
- ]
+ ])
end
# Only group owner and administrators can manage group
if group.has_owner?(user) || user.admin?
- rules += [
+ rules.push(*[
:manage_group,
:manage_namespace
- ]
+ ])
end
rules.flatten
@@ -214,10 +214,10 @@ class Ability
# Only namespace owner and administrators can manage it
if namespace.owner == user || user.admin?
- rules += [
+ rules.push(*[
:create_projects,
:manage_namespace
- ]
+ ])
end
rules.flatten
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
new file mode 100644
index 00000000000..f1d918e5457
--- /dev/null
+++ b/app/models/application_setting.rb
@@ -0,0 +1,43 @@
+# == Schema Information
+#
+# Table name: application_settings
+#
+# id :integer not null, primary key
+# default_projects_limit :integer
+# default_branch_protection :integer
+# signup_enabled :boolean
+# signin_enabled :boolean
+# gravatar_enabled :boolean
+# twitter_sharing_enabled :boolean
+# sign_in_text :text
+# created_at :datetime
+# updated_at :datetime
+# home_page_url :string(255)
+#
+
+class ApplicationSetting < ActiveRecord::Base
+ validates :home_page_url,
+ allow_blank: true,
+ format: { with: URI::regexp(%w(http https)), message: "should be a valid url" },
+ if: :home_page_url_column_exist
+
+ def self.current
+ ApplicationSetting.last
+ end
+
+ def self.create_from_defaults
+ create(
+ default_projects_limit: Settings.gitlab['default_projects_limit'],
+ default_branch_protection: Settings.gitlab['default_branch_protection'],
+ signup_enabled: Settings.gitlab['signup_enabled'],
+ signin_enabled: Settings.gitlab['signin_enabled'],
+ twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'],
+ gravatar_enabled: Settings.gravatar['enabled'],
+ sign_in_text: Settings.extra['sign_in_text'],
+ )
+ end
+
+ def home_page_url_column_exist
+ ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url)
+ end
+end
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index 4d0c04bcc3d..05f5e979695 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -14,6 +14,8 @@
#
class BroadcastMessage < ActiveRecord::Base
+ include Sortable
+
validates :message, presence: true
validates :starts_at, presence: true
validates :ends_at, presence: true
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 212229649fc..e0461809e10 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -10,12 +10,12 @@ class Commit
# Used to prevent 500 error on huge commits by suppressing diff
#
# User can force display of diff above this size
- DIFF_SAFE_FILES = 100
- DIFF_SAFE_LINES = 5000
+ DIFF_SAFE_FILES = 100 unless defined?(DIFF_SAFE_FILES)
+ DIFF_SAFE_LINES = 5000 unless defined?(DIFF_SAFE_LINES)
# Commits above this size will not be rendered in HTML
- DIFF_HARD_LIMIT_FILES = 1000
- DIFF_HARD_LIMIT_LINES = 50000
+ DIFF_HARD_LIMIT_FILES = 1000 unless defined?(DIFF_HARD_LIMIT_FILES)
+ DIFF_HARD_LIMIT_LINES = 50000 unless defined?(DIFF_HARD_LIMIT_LINES)
class << self
def decorate(commits)
@@ -75,11 +75,11 @@ class Commit
return no_commit_message if title.blank?
- title_end = title.index(/\n/)
+ title_end = title.index("\n")
if (!title_end && title.length > 100) || (title_end && title_end > 100)
title[0..79] << "&hellip;".html_safe
else
- title.split(/\n/, 2).first
+ title.split("\n", 2).first
end
end
@@ -87,12 +87,13 @@ class Commit
#
# cut off, ellipses (`&hellp;`) are prepended to the commit message.
def description
- title_end = safe_message.index(/\n/)
- @description ||= if (!title_end && safe_message.length > 100) || (title_end && title_end > 100)
- "&hellip;".html_safe << safe_message[80..-1]
- else
- safe_message.split(/\n/, 2)[1].try(:chomp)
- end
+ title_end = safe_message.index("\n")
+ @description ||=
+ if (!title_end && safe_message.length > 100) || (title_end && title_end > 100)
+ "&hellip;".html_safe << safe_message[80..-1]
+ else
+ safe_message.split("\n", 2)[1].try(:chomp)
+ end
end
def description?
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 553087946d6..f5e23e9dc2d 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -29,6 +29,8 @@ module Issuable
scope :only_opened, -> { with_state(:opened) }
scope :only_reopened, -> { with_state(:reopened) }
scope :closed, -> { with_state(:closed) }
+ scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') }
+ scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') }
delegate :name,
:email,
@@ -55,13 +57,10 @@ module Issuable
def sort(method)
case method.to_s
- when 'newest' then reorder("#{table_name}.created_at DESC")
- when 'oldest' then reorder("#{table_name}.created_at ASC")
- when 'recently_updated' then reorder("#{table_name}.updated_at DESC")
- when 'last_updated' then reorder("#{table_name}.updated_at ASC")
- when 'milestone_due_soon' then joins(:milestone).reorder("milestones.due_date ASC")
- when 'milestone_due_later' then joins(:milestone).reorder("milestones.due_date DESC")
- else reorder("#{table_name}.created_at DESC")
+ when 'milestone_due_asc' then order_milestone_due_asc
+ when 'milestone_due_desc' then order_milestone_due_desc
+ else
+ order_by(method)
end
end
end
@@ -88,7 +87,7 @@ module Issuable
# Return the number of -1 comments (downvotes)
def downvotes
- notes.select(&:downvote?).size
+ filter_superceded_votes(notes.select(&:downvote?), notes).size
end
def downvotes_in_percent
@@ -101,7 +100,7 @@ module Issuable
# Return the number of +1 comments (upvotes)
def upvotes
- notes.select(&:upvote?).size
+ filter_superceded_votes(notes.select(&:upvote?), notes).size
end
def upvotes_in_percent
@@ -124,16 +123,19 @@ module Issuable
users << assignee if is_assigned?
mentions = []
mentions << self.mentioned_users
+
notes.each do |note|
users << note.author
mentions << note.mentioned_users
end
+
users.concat(mentions.reduce([], :|)).uniq
end
- def to_hook_data
+ def to_hook_data(user)
{
object_kind: self.class.name.underscore,
+ user: user.hook_attrs,
object_attributes: hook_attrs
}
end
@@ -148,9 +150,23 @@ module Issuable
def add_labels_by_names(label_names)
label_names.each do |label_name|
- label = project.labels.create_with(
- color: Label::DEFAULT_COLOR).find_or_create_by(title: label_name.strip)
+ label = project.labels.create_with(color: Label::DEFAULT_COLOR).
+ find_or_create_by(title: label_name.strip)
self.labels << label
end
end
+
+ private
+
+ def filter_superceded_votes(votes, notes)
+ filteredvotes = [] + votes
+
+ votes.each do |vote|
+ if vote.superceded?(notes)
+ filteredvotes.delete(vote)
+ end
+ end
+
+ filteredvotes
+ end
end
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 6c1aa99668a..50be458bf24 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -50,10 +50,13 @@ module Mentionable
matches.each do |match|
identifier = match.delete "@"
if identifier == "all"
- users += project.team.members.flatten
- else
- id = User.find_by(username: identifier).try(:id)
- users << User.find(id) unless id.blank?
+ users.push(*project.team.members.flatten)
+ elsif namespace = Namespace.find_by(path: identifier)
+ if namespace.type == "Group"
+ users.push(*namespace.users)
+ else
+ users << namespace.owner
+ end
end
end
users.uniq
@@ -64,9 +67,10 @@ module Mentionable
return [] if text.blank?
ext = Gitlab::ReferenceExtractor.new
ext.analyze(text, p)
- (ext.issues_for +
- ext.merge_requests_for +
- ext.commits_for).uniq - [local_reference]
+
+ (ext.issues_for(p) +
+ ext.merge_requests_for(p) +
+ ext.commits_for(p)).uniq - [local_reference]
end
# Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+.
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
new file mode 100644
index 00000000000..0ad2654867d
--- /dev/null
+++ b/app/models/concerns/sortable.rb
@@ -0,0 +1,35 @@
+# == Sortable concern
+#
+# Set default scope for ordering objects
+#
+module Sortable
+ extend ActiveSupport::Concern
+
+ included do
+ # By default all models should be ordered
+ # by created_at field starting from newest
+ default_scope { order(created_at: :desc, id: :desc) }
+
+ scope :order_created_desc, -> { reorder(created_at: :desc, id: :desc) }
+ scope :order_created_asc, -> { reorder(created_at: :asc, id: :asc) }
+ scope :order_updated_desc, -> { reorder(updated_at: :desc, id: :desc) }
+ scope :order_updated_asc, -> { reorder(updated_at: :asc, id: :asc) }
+ scope :order_name_asc, -> { reorder(name: :asc) }
+ scope :order_name_desc, -> { reorder(name: :desc) }
+ end
+
+ module ClassMethods
+ def order_by(method)
+ case method.to_s
+ when 'name_asc' then order_name_asc
+ when 'name_desc' then order_name_desc
+ when 'updated_asc' then order_updated_asc
+ when 'updated_desc' then order_updated_desc
+ when 'created_asc' then order_created_asc
+ when 'created_desc' then order_created_desc
+ else
+ all
+ end
+ end
+ end
+end
diff --git a/app/models/email.rb b/app/models/email.rb
index 57f476bd519..556b0e9586e 100644
--- a/app/models/email.rb
+++ b/app/models/email.rb
@@ -10,6 +10,8 @@
#
class Email < ActiveRecord::Base
+ include Sortable
+
belongs_to :user
validates :user_id, presence: true
diff --git a/app/models/event.rb b/app/models/event.rb
index c0b126713a6..5579ab1dbb0 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -15,6 +15,7 @@
#
class Event < ActiveRecord::Base
+ include Sortable
default_scope { where.not(author_id: nil) }
CREATED = 1
@@ -46,31 +47,9 @@ class Event < ActiveRecord::Base
scope :recent, -> { order("created_at DESC") }
scope :code_push, -> { where(action: PUSHED) }
scope :in_projects, ->(project_ids) { where(project_id: project_ids).recent }
+ scope :with_associations, -> { includes(project: :namespace) }
class << self
- def create_ref_event(project, user, ref, action = 'add', prefix = 'refs/heads')
- commit = project.repository.commit(ref.target)
-
- if action.to_s == 'add'
- before = '00000000'
- after = commit.id
- else
- before = commit.id
- after = '00000000'
- end
-
- Event.create(
- project: project,
- action: Event::PUSHED,
- data: {
- ref: "#{prefix}/#{ref.name}",
- before: before,
- after: after
- },
- author_id: user.id
- )
- end
-
def reset_event_cache_for(target)
Event.where(target_id: target.id, target_type: target.class.to_s).
order('id DESC').limit(100).
@@ -83,6 +62,8 @@ class Event < ActiveRecord::Base
true
elsif membership_changed?
true
+ elsif created_project?
+ true
else
(issue? || merge_request? || note? || milestone?) && target
end
@@ -97,25 +78,51 @@ class Event < ActiveRecord::Base
end
def target_title
- if target && target.respond_to?(:title)
- target.title
- end
+ target.title if target && target.respond_to?(:title)
+ end
+
+ def created?
+ action == CREATED
end
def push?
- action == self.class::PUSHED && valid_push?
+ action == PUSHED && valid_push?
end
def merged?
- action == self.class::MERGED
+ action == MERGED
end
def closed?
- action == self.class::CLOSED
+ action == CLOSED
end
def reopened?
- action == self.class::REOPENED
+ action == REOPENED
+ end
+
+ def joined?
+ action == JOINED
+ end
+
+ def left?
+ action == LEFT
+ end
+
+ def commented?
+ action == COMMENTED
+ end
+
+ def membership_changed?
+ joined? || left?
+ end
+
+ def created_project?
+ created? && !target
+ end
+
+ def created_target?
+ created? && target
end
def milestone?
@@ -134,32 +141,32 @@ class Event < ActiveRecord::Base
target_type == "MergeRequest"
end
- def joined?
- action == JOINED
- end
-
- def left?
- action == LEFT
- end
-
- def membership_changed?
- joined? || left?
+ def milestone
+ target if milestone?
end
def issue
- target if target_type == "Issue"
+ target if issue?
end
def merge_request
- target if target_type == "MergeRequest"
+ target if merge_request?
end
def note
- target if target_type == "Note"
+ target if note?
end
def action_name
- if closed?
+ if push?
+ if new_ref?
+ "pushed new"
+ elsif rm_ref?
+ "deleted"
+ else
+ "pushed to"
+ end
+ elsif closed?
"closed"
elsif merged?
"accepted"
@@ -167,6 +174,10 @@ class Event < ActiveRecord::Base
'joined'
elsif left?
'left'
+ elsif commented?
+ "commented on"
+ elsif created_project?
+ "created"
else
"opened"
end
@@ -174,7 +185,7 @@ class Event < ActiveRecord::Base
def valid_push?
data[:ref] && ref_name.present?
- rescue => ex
+ rescue
false
end
@@ -186,10 +197,6 @@ class Event < ActiveRecord::Base
data[:ref]["refs/heads"]
end
- def new_branch?
- commit_from =~ /^00000/
- end
-
def new_ref?
commit_from =~ /^00000/
end
@@ -239,16 +246,6 @@ class Event < ActiveRecord::Base
tag? ? "tag" : "branch"
end
- def push_action_name
- if new_ref?
- "pushed new"
- elsif rm_ref?
- "deleted"
- else
- "pushed to"
- end
- end
-
def push_with_commits?
md_ref? && commits.any? && commit_from && commit_to
end
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
new file mode 100644
index 00000000000..50efcb32f1b
--- /dev/null
+++ b/app/models/external_issue.rb
@@ -0,0 +1,25 @@
+class ExternalIssue
+ def initialize(issue_identifier, project)
+ @issue_identifier, @project = issue_identifier, project
+ end
+
+ def to_s
+ @issue_identifier.to_s
+ end
+
+ def id
+ @issue_identifier.to_s
+ end
+
+ def iid
+ @issue_identifier.to_s
+ end
+
+ def ==(other)
+ other.is_a?(self.class) && (to_s == other.to_s)
+ end
+
+ def project
+ @project
+ end
+end
diff --git a/app/models/group.rb b/app/models/group.rb
index b8ed3b8ac73..da9621a2a1a 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -21,9 +21,22 @@ class Group < Namespace
has_many :users, through: :group_members
validate :avatar_type, if: ->(user) { user.avatar_changed? }
- validates :avatar, file_size: { maximum: 100.kilobytes.to_i }
+ validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
- mount_uploader :avatar, AttachmentUploader
+ mount_uploader :avatar, AvatarUploader
+
+ after_create :post_create_hook
+ after_destroy :post_destroy_hook
+
+ class << self
+ def search(query)
+ where("LOWER(namespaces.name) LIKE :query or LOWER(namespaces.path) LIKE :query", query: "%#{query.downcase}%")
+ end
+
+ def sort(method)
+ order_by(method)
+ end
+ end
def human_name
name
@@ -74,19 +87,15 @@ class Group < Namespace
projects.public_only.any?
end
- class << self
- def search(query)
- where("LOWER(namespaces.name) LIKE :query", query: "%#{query.downcase}%")
- end
+ def post_create_hook
+ system_hook_service.execute_hooks_for(self, :create)
+ end
- def sort(method)
- case method.to_s
- when "newest" then reorder("namespaces.created_at DESC")
- when "oldest" then reorder("namespaces.created_at ASC")
- when "recently_updated" then reorder("namespaces.updated_at DESC")
- when "last_updated" then reorder("namespaces.updated_at ASC")
- else reorder("namespaces.path, namespaces.name ASC")
- end
- end
+ def post_destroy_hook
+ system_hook_service.execute_hooks_for(self, :destroy)
+ end
+
+ def system_hook_service
+ SystemHooksService.new
end
end
diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb
index 33915313789..7e4f16ebf16 100644
--- a/app/models/group_milestone.rb
+++ b/app/models/group_milestone.rb
@@ -66,15 +66,15 @@ class GroupMilestone
end
def issues
- @group_issues ||= milestones.map { |milestone| milestone.issues }.flatten.group_by(&:state)
+ @group_issues ||= milestones.map(&:issues).flatten.group_by(&:state)
end
def merge_requests
- @group_merge_requests ||= milestones.map { |milestone| milestone.merge_requests }.flatten.group_by(&:state)
+ @group_merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state)
end
def participants
- milestones.map { |milestone| milestone.participants.uniq }.reject(&:empty?).flatten
+ @group_participants ||= milestones.map(&:participants).flatten.compact.uniq
end
def opened_issues
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 23fa01e0b70..defef7216f2 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -16,6 +16,7 @@
#
class WebHook < ActiveRecord::Base
+ include Sortable
include HTTParty
default_value_for :push_events, true
@@ -32,7 +33,10 @@ class WebHook < ActiveRecord::Base
def execute(data)
parsed_url = URI.parse(url)
if parsed_url.userinfo.blank?
- WebHook.post(url, body: data.to_json, headers: { "Content-Type" => "application/json" }, verify: false)
+ WebHook.post(url,
+ body: data.to_json,
+ headers: { "Content-Type" => "application/json" },
+ verify: false)
else
post_url = url.gsub("#{parsed_url.userinfo}@", "")
auth = {
@@ -41,10 +45,13 @@ class WebHook < ActiveRecord::Base
}
WebHook.post(post_url,
body: data.to_json,
- headers: {"Content-Type" => "application/json"},
+ headers: { "Content-Type" => "application/json" },
verify: false,
basic_auth: auth)
end
+ rescue SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e
+ logger.error("WebHook Error => #{e}")
+ false
end
def async_execute(data)
diff --git a/app/models/identity.rb b/app/models/identity.rb
new file mode 100644
index 00000000000..b2c3792d1ce
--- /dev/null
+++ b/app/models/identity.rb
@@ -0,0 +1,16 @@
+# == Schema Information
+#
+# Table name: identities
+#
+# id :integer not null, primary key
+# extern_uid :string(255)
+# provider :string(255)
+# user_id :integer
+#
+
+class Identity < ActiveRecord::Base
+ include Sortable
+ belongs_to :user
+
+ validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider }
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 8a9e969248c..19e43ebd788 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -24,6 +24,7 @@ class Issue < ActiveRecord::Base
include Issuable
include InternalId
include Taskable
+ include Sortable
ActsAsTaggableOn.strict_case_match = true
diff --git a/app/models/key.rb b/app/models/key.rb
index 095c73d8baf..e2e59296eed 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -15,11 +15,12 @@
require 'digest/md5'
class Key < ActiveRecord::Base
+ include Sortable
include Gitlab::Popen
belongs_to :user
- before_validation :strip_white_space, :generate_fingerpint
+ before_validation :strip_white_space, :generate_fingerprint
validates :title, presence: true, length: { within: 0..255 }
validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ }, uniqueness: true
@@ -76,7 +77,7 @@ class Key < ActiveRecord::Base
private
- def generate_fingerpint
+ def generate_fingerprint
self.fingerprint = nil
return unless key.present?
@@ -89,7 +90,7 @@ class Key < ActiveRecord::Base
end
if cmd_status.zero?
- cmd_output.gsub /([\d\h]{2}:)+[\d\h]{2}/ do |match|
+ cmd_output.gsub /(\h{2}:)+\h{2}/ do |match|
self.fingerprint = match
end
end
diff --git a/app/models/label.rb b/app/models/label.rb
index 2b2b02e0645..9d7099c5652 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -28,7 +28,7 @@ class Label < ActiveRecord::Base
format: { with: /\A[^&\?,&]+\z/ },
uniqueness: { scope: :project_id }
- scope :order_by_name, -> { reorder("labels.title ASC") }
+ default_scope { order(title: :asc) }
alias_attribute :name, :title
diff --git a/app/models/member.rb b/app/models/member.rb
index 671ef466baa..fe3d2f40e87 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -14,6 +14,7 @@
#
class Member < ActiveRecord::Base
+ include Sortable
include Notifiable
include Gitlab::Access
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index b7f296b13fb..28d0b4483b4 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -27,8 +27,9 @@ class GroupMember < Member
scope :with_group, ->(group) { where(source_id: group.id) }
scope :with_user, ->(user) { where(user_id: user.id) }
- after_create :notify_create
+ after_create :post_create_hook
after_update :notify_update
+ after_destroy :post_destroy_hook
def self.access_level_roles
Gitlab::Access.options_with_owner
@@ -42,8 +43,9 @@ class GroupMember < Member
access_level
end
- def notify_create
+ def post_create_hook
notification_service.new_group_member(self)
+ system_hook_service.execute_hooks_for(self, :create)
end
def notify_update
@@ -52,6 +54,14 @@ class GroupMember < Member
end
end
+ def post_destroy_hook
+ system_hook_service.execute_hooks_for(self, :destroy)
+ end
+
+ def system_hook_service
+ SystemHooksService.new
+ end
+
def notification_service
NotificationService.new
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 30c09f768d7..e4791d0f0aa 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -114,13 +114,11 @@ class ProjectMember < Member
end
def post_create_hook
- Event.create(
- project_id: self.project.id,
- action: Event::JOINED,
- author_id: self.user.id
- )
-
- notification_service.new_team_member(self) unless owner?
+ unless owner?
+ event_service.join_project(self.project, self.user)
+ notification_service.new_team_member(self)
+ end
+
system_hook_service.execute_hooks_for(self, :create)
end
@@ -129,15 +127,14 @@ class ProjectMember < Member
end
def post_destroy_hook
- Event.create(
- project_id: self.project.id,
- action: Event::LEFT,
- author_id: self.user.id
- )
-
+ event_service.leave_project(self.project, self.user)
system_hook_service.execute_hooks_for(self, :destroy)
end
+ def event_service
+ EventCreateService.new
+ end
+
def notification_service
NotificationService.new
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 7c525b02f48..f758126cfeb 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -18,6 +18,7 @@
# iid :integer
# description :text
# position :integer default(0)
+# locked_at :datetime
#
require Rails.root.join("app/models/commit")
@@ -27,6 +28,7 @@ class MergeRequest < ActiveRecord::Base
include Issuable
include Taskable
include InternalId
+ include Sortable
belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project"
belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project"
@@ -70,6 +72,16 @@ class MergeRequest < ActiveRecord::Base
transition locked: :reopened
end
+ after_transition any => :locked do |merge_request, transition|
+ merge_request.locked_at = Time.now
+ merge_request.save
+ end
+
+ after_transition locked: (any - :locked) do |merge_request, transition|
+ merge_request.locked_at = nil
+ merge_request.save
+ end
+
state :opened
state :reopened
state :closed
@@ -179,7 +191,9 @@ class MergeRequest < ActiveRecord::Base
end
def automerge!(current_user, commit_message = nil)
- MergeRequests::AutoMergeService.new.execute(self, current_user, commit_message)
+ MergeRequests::AutoMergeService.
+ new(target_project, current_user).
+ execute(self, commit_message)
end
def open?
@@ -238,7 +252,8 @@ class MergeRequest < ActiveRecord::Base
def closes_issues
if target_branch == project.default_branch
issues = commits.flat_map { |c| c.closes_issues(project) }
- issues += Gitlab::ClosingIssueExtractor.closed_by_message_in_project(description, project)
+ issues.push(*Gitlab::ClosingIssueExtractor.
+ closed_by_message_in_project(description, project))
issues.uniq.sort_by(&:id)
else
[]
@@ -318,7 +333,7 @@ class MergeRequest < ActiveRecord::Base
end
# Return array of possible target branches
- # dependes on target project of MR
+ # depends on target project of MR
def target_branches
if target_project.nil?
[]
@@ -328,7 +343,7 @@ class MergeRequest < ActiveRecord::Base
end
# Return array of possible source branches
- # dependes on source project of MR
+ # depends on source project of MR
def source_branches
if source_project.nil?
[]
@@ -336,4 +351,8 @@ class MergeRequest < ActiveRecord::Base
source_project.repository.branch_names
end
end
+
+ def locked_long_ago?
+ locked_at && locked_at < (Time.now - 1.day)
+ end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index a71122d5e07..acac1ca4cf7 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -14,6 +14,8 @@
require Rails.root.join("app/models/commit")
class MergeRequestDiff < ActiveRecord::Base
+ include Sortable
+
# Prevent store of diff
# if commits amount more then 200
COMMITS_SAFE_SIZE = 200
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 8fd3e56d2ee..9bbb2bafb98 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -15,6 +15,7 @@
class Milestone < ActiveRecord::Base
include InternalId
+ include Sortable
belongs_to :project
has_many :issues
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index c0c6de0ee7d..2c7ed376265 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -14,21 +14,27 @@
#
class Namespace < ActiveRecord::Base
+ include Sortable
include Gitlab::ShellAdapter
has_many :projects, dependent: :destroy
belongs_to :owner, class_name: "User"
validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
- validates :name, presence: true, uniqueness: true,
- length: { within: 0..255 },
- format: { with: Gitlab::Regex.name_regex,
- message: Gitlab::Regex.name_regex_message }
+ validates :name,
+ presence: true, uniqueness: true,
+ length: { within: 0..255 },
+ format: { with: Gitlab::Regex.name_regex,
+ message: Gitlab::Regex.name_regex_message }
+
validates :description, length: { within: 0..255 }
- validates :path, uniqueness: { case_sensitive: false }, presence: true, length: { within: 1..255 },
- exclusion: { in: Gitlab::Blacklist.path },
- format: { with: Gitlab::Regex.path_regex,
- message: Gitlab::Regex.path_regex_message }
+ validates :path,
+ uniqueness: { case_sensitive: false },
+ presence: true,
+ length: { within: 1..255 },
+ exclusion: { in: Gitlab::Blacklist.path },
+ format: { with: Gitlab::Regex.path_regex,
+ message: Gitlab::Regex.path_regex_message }
delegate :name, to: :owner, allow_nil: true, prefix: true
@@ -38,6 +44,10 @@ class Namespace < ActiveRecord::Base
scope :root, -> { where('type IS NULL') }
+ def self.by_path(path)
+ where('lower(path) = :value', value: path.downcase).first
+ end
+
def self.search(query)
where("name LIKE :query OR path LIKE :query", query: "%#{query}%")
end
@@ -90,4 +100,8 @@ class Namespace < ActiveRecord::Base
def kind
type == 'Group' ? 'group' : 'user'
end
+
+ def find_fork_of(project)
+ projects.joins(:forked_project_link).where('forked_project_links.forked_from_project_id = ?', project.id).first
+ end
end
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index 43979b5e807..f4e90125373 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -84,7 +84,7 @@ module Network
skip += self.class.max_count
end
else
- # Cant't find the target commit in the repo.
+ # Can't find the target commit in the repo.
offset = 0
end
end
@@ -226,7 +226,7 @@ module Network
reserved = []
for day in time_range
- reserved += @reserved[day]
+ reserved.push(*@reserved[day])
end
reserved.uniq!
diff --git a/app/models/note.rb b/app/models/note.rb
index f0ed7580b4c..e6c258ffbe9 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -49,7 +49,7 @@ class Note < ActiveRecord::Base
scope :not_inline, ->{ where(line_code: [nil, '']) }
scope :system, ->{ where(system: true) }
scope :common, ->{ where(noteable_type: ["", nil]) }
- scope :fresh, ->{ order("created_at ASC, id ASC") }
+ scope :fresh, ->{ order(created_at: :asc, id: :asc) }
scope :inc_author_project, ->{ includes(:project, :author) }
scope :inc_author, ->{ includes(:author) }
@@ -90,7 +90,7 @@ class Note < ActiveRecord::Base
note_options.merge!(noteable: noteable)
end
- create(note_options)
+ create(note_options) unless cross_reference_disallowed?(noteable, mentioner)
end
def create_milestone_change_note(noteable, project, author, milestone)
@@ -121,6 +121,36 @@ class Note < ActiveRecord::Base
})
end
+ def create_labels_change_note(noteable, project, author, added_labels, removed_labels)
+ labels_count = added_labels.count + removed_labels.count
+ added_labels = added_labels.map{ |label| "~#{label.id}" }.join(' ')
+ removed_labels = removed_labels.map{ |label| "~#{label.id}" }.join(' ')
+ message = ''
+
+ if added_labels.present?
+ message << "added #{added_labels}"
+ end
+
+ if added_labels.present? && removed_labels.present?
+ message << ' and '
+ end
+
+ if removed_labels.present?
+ message << "removed #{removed_labels}"
+ end
+
+ message << ' ' << 'label'.pluralize(labels_count)
+ body = "_#{message.capitalize}_"
+
+ create(
+ noteable: noteable,
+ project: project,
+ author: author,
+ note: body,
+ system: true
+ )
+ end
+
def create_new_commits_note(noteable, project, author, commits)
commits_text = ActionController::Base.helpers.pluralize(commits.size, 'new commit')
body = "Added #{commits_text}:\n\n"
@@ -165,6 +195,15 @@ class Note < ActiveRecord::Base
[:discussion, type.try(:underscore), id, line_code].join("-").to_sym
end
+ # Determine if cross reference note should be created.
+ # eg. mentioning a commit in MR comments which exists inside a MR
+ # should not create "mentioned in" note.
+ def cross_reference_disallowed?(noteable, mentioner)
+ if mentioner.kind_of?(MergeRequest)
+ mentioner.commits.map(&:id).include? noteable.id
+ end
+ end
+
# Determine whether or not a cross-reference note already exists.
def cross_reference_exists?(noteable, mentioner)
gfm_reference = mentioner_gfm_ref(noteable, mentioner)
@@ -251,8 +290,8 @@ class Note < ActiveRecord::Base
def commit_author
@commit_author ||=
- project.users.find_by(email: noteable.author_email) ||
- project.users.find_by(name: noteable.author_name)
+ project.team.users.find_by(email: noteable.author_email) ||
+ project.team.users.find_by(name: noteable.author_name)
rescue
nil
end
@@ -287,6 +326,7 @@ class Note < ActiveRecord::Base
# If not - its outdated diff
def active?
return true unless self.diff
+ return false unless noteable
noteable.diffs.each do |mr_diff|
next unless mr_diff.new_path == self.diff.new_path
@@ -308,7 +348,7 @@ class Note < ActiveRecord::Base
end
def diff_file_index
- line_code.split('_')[0]
+ line_code.split('_')[0] if line_code
end
def diff_file_name
@@ -324,11 +364,11 @@ class Note < ActiveRecord::Base
end
def diff_old_line
- line_code.split('_')[1].to_i
+ line_code.split('_')[1].to_i if line_code
end
def diff_new_line
- line_code.split('_')[2].to_i
+ line_code.split('_')[2].to_i if line_code
end
def generate_line_code(line)
@@ -349,25 +389,39 @@ class Note < ActiveRecord::Base
@diff_line
end
+ def diff_line_type
+ return @diff_line_type if @diff_line_type
+
+ if diff
+ diff_lines.each do |line|
+ if generate_line_code(line) == self.line_code
+ @diff_line_type = line.type
+ end
+ end
+ end
+
+ @diff_line_type
+ end
+
def truncated_diff_lines
max_number_of_lines = 16
prev_match_line = nil
prev_lines = []
diff_lines.each do |line|
- if generate_line_code(line) != self.line_code
- if line.type == "match"
- prev_lines.clear
- prev_match_line = line
- else
- prev_lines.push(line)
- prev_lines.shift if prev_lines.length >= max_number_of_lines
- end
+ if line.type == "match"
+ prev_lines.clear
+ prev_match_line = line
else
prev_lines << line
- return prev_lines
+
+ break if generate_line_code(line) == self.line_code
+
+ prev_lines.shift if prev_lines.length >= max_number_of_lines
end
end
+
+ prev_lines
end
def diff_lines
@@ -435,6 +489,26 @@ class Note < ActiveRecord::Base
)
end
+ def superceded?(notes)
+ return false unless vote?
+
+ notes.each do |note|
+ next if note == self
+
+ if note.vote? &&
+ self[:author_id] == note[:author_id] &&
+ self[:created_at] <= note[:created_at]
+ return true
+ end
+ end
+
+ false
+ end
+
+ def vote?
+ upvote? || downvote?
+ end
+
def votable?
for_issue? || (for_merge_request? && !for_diff_line?)
end
@@ -456,7 +530,7 @@ class Note < ActiveRecord::Base
end
# FIXME: Hack for polymorphic associations with STI
- # For more information wisit http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations
+ # For more information visit http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations
def noteable_type=(sType)
super(sType.to_s.classify.constantize.base_class.to_s)
end
@@ -479,6 +553,6 @@ class Note < ActiveRecord::Base
end
def editable?
- !system
+ !read_attribute(:system)
end
end
diff --git a/app/models/notification.rb b/app/models/notification.rb
index b0f8ed6a4ec..1395274173d 100644
--- a/app/models/notification.rb
+++ b/app/models/notification.rb
@@ -6,12 +6,13 @@ class Notification
N_PARTICIPATING = 1
N_WATCH = 2
N_GLOBAL = 3
+ N_MENTION = 4
attr_accessor :target
class << self
def notification_levels
- [N_DISABLED, N_PARTICIPATING, N_WATCH]
+ [N_DISABLED, N_PARTICIPATING, N_WATCH, N_MENTION]
end
def options_with_labels
@@ -19,12 +20,13 @@ class Notification
disabled: N_DISABLED,
participating: N_PARTICIPATING,
watch: N_WATCH,
+ mention: N_MENTION,
global: N_GLOBAL
}
end
def project_notification_levels
- [N_DISABLED, N_PARTICIPATING, N_WATCH, N_GLOBAL]
+ [N_DISABLED, N_PARTICIPATING, N_WATCH, N_GLOBAL, N_MENTION]
end
end
@@ -48,6 +50,10 @@ class Notification
target.notification_level == N_GLOBAL
end
+ def mention?
+ target.notification_level == N_MENTION
+ end
+
def level
target.notification_level
end
diff --git a/app/models/project.rb b/app/models/project.rb
index c58c9b551c9..d33b25db201 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -24,9 +24,16 @@
# import_status :string(255)
# repository_size :float default(0.0)
# star_count :integer default(0), not null
+# import_type :string(255)
+# import_source :string(255)
+# avatar :string(255)
#
+require 'carrierwave/orm/activerecord'
+require 'file_size_validator'
+
class Project < ActiveRecord::Base
+ include Sortable
include Gitlab::ShellAdapter
include Gitlab::VisibilityLevel
include Gitlab::ConfigHelper
@@ -41,14 +48,20 @@ class Project < ActiveRecord::Base
default_value_for :wall_enabled, false
default_value_for :snippets_enabled, gitlab_config_features.snippets
+ # set last_activity_at to the same as created_at
+ after_create :set_last_activity_at
+ def set_last_activity_at
+ update_column(:last_activity_at, self.created_at)
+ end
+
ActsAsTaggableOn.strict_case_match = true
acts_as_taggable_on :tags
attr_accessor :new_default_branch
# Relations
- belongs_to :creator, foreign_key: "creator_id", class_name: "User"
- belongs_to :group, -> { where(type: Group) }, foreign_key: "namespace_id"
+ belongs_to :creator, foreign_key: 'creator_id', class_name: 'User'
+ belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id'
belongs_to :namespace
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id'
@@ -62,25 +75,33 @@ class Project < ActiveRecord::Base
has_one :hipchat_service, dependent: :destroy
has_one :flowdock_service, dependent: :destroy
has_one :assembla_service, dependent: :destroy
+ has_one :asana_service, dependent: :destroy
has_one :gemnasium_service, dependent: :destroy
has_one :slack_service, dependent: :destroy
has_one :buildbox_service, dependent: :destroy
has_one :bamboo_service, dependent: :destroy
+ has_one :teamcity_service, dependent: :destroy
has_one :pushover_service, dependent: :destroy
+ has_one :jira_service, dependent: :destroy
+ has_one :redmine_service, dependent: :destroy
+ has_one :custom_issue_tracker_service, dependent: :destroy
+ has_one :gitlab_issue_tracker_service, dependent: :destroy
+
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
+
has_one :forked_from_project, through: :forked_project_link
# Merge Requests for target project should be removed with it
- has_many :merge_requests, dependent: :destroy, foreign_key: "target_project_id"
+ has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id'
# Merge requests from source project should be kept when source project was removed
- has_many :fork_merge_requests, foreign_key: "source_project_id", class_name: MergeRequest
- has_many :issues, -> { order 'issues.state DESC, issues.created_at DESC' }, dependent: :destroy
+ has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: MergeRequest
+ has_many :issues, dependent: :destroy
has_many :labels, dependent: :destroy
has_many :services, dependent: :destroy
has_many :events, dependent: :destroy
has_many :milestones, dependent: :destroy
has_many :notes, dependent: :destroy
- has_many :snippets, dependent: :destroy, class_name: "ProjectSnippet"
- has_many :hooks, dependent: :destroy, class_name: "ProjectHook"
+ has_many :snippets, dependent: :destroy, class_name: 'ProjectSnippet'
+ has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
has_many :protected_branches, dependent: :destroy
has_many :project_members, dependent: :destroy, as: :source, class_name: 'ProjectMember'
has_many :users, through: :project_members
@@ -95,13 +116,16 @@ class Project < ActiveRecord::Base
# Validations
validates :creator, presence: true, on: :create
validates :description, length: { maximum: 2000 }, allow_blank: true
- validates :name, presence: true, length: { within: 0..255 },
- format: { with: Gitlab::Regex.project_name_regex,
- message: Gitlab::Regex.project_regex_message }
- validates :path, presence: true, length: { within: 0..255 },
- exclusion: { in: Gitlab::Blacklist.path },
- format: { with: Gitlab::Regex.path_regex,
- message: Gitlab::Regex.path_regex_message }
+ validates :name,
+ presence: true,
+ length: { within: 0..255 },
+ format: { with: Gitlab::Regex.project_name_regex,
+ message: Gitlab::Regex.project_regex_message }
+ validates :path,
+ presence: true,
+ length: { within: 0..255 },
+ format: { with: Gitlab::Regex.path_regex,
+ message: Gitlab::Regex.path_regex_message }
validates :issues_enabled, :merge_requests_enabled,
:wiki_enabled, inclusion: { in: [true, false] }
validates :visibility_level,
@@ -112,50 +136,55 @@ class Project < ActiveRecord::Base
validates_uniqueness_of :name, scope: :namespace_id
validates_uniqueness_of :path, scope: :namespace_id
validates :import_url,
- format: { with: URI::regexp(%w(git http https)), message: "should be a valid url" },
+ format: { with: URI::regexp(%w(ssh git http https)), message: 'should be a valid url' },
if: :import?
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create
+ validate :avatar_type,
+ if: ->(project) { project.avatar && project.avatar_changed? }
+ validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
+
+ mount_uploader :avatar, AvatarUploader
# Scopes
- scope :without_user, ->(user) { where("projects.id NOT IN (:ids)", ids: user.authorized_projects.map(&:id) ) }
- scope :without_team, ->(team) { team.projects.present? ? where("projects.id NOT IN (:ids)", ids: team.projects.map(&:id)) : scoped }
- scope :not_in_group, ->(group) { where("projects.id NOT IN (:ids)", ids: group.project_ids ) }
- scope :in_team, ->(team) { where("projects.id IN (:ids)", ids: team.projects.map(&:id)) }
+ scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
+ scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
+ scope :sorted_by_names, -> { joins(:namespace).reorder('namespaces.name ASC, projects.name ASC') }
+
+ scope :without_user, ->(user) { where('projects.id NOT IN (:ids)', ids: user.authorized_projects.map(&:id) ) }
+ scope :without_team, ->(team) { team.projects.present? ? where('projects.id NOT IN (:ids)', ids: team.projects.map(&:id)) : scoped }
+ scope :not_in_group, ->(group) { where('projects.id NOT IN (:ids)', ids: group.project_ids ) }
+ scope :in_team, ->(team) { where('projects.id IN (:ids)', ids: team.projects.map(&:id)) }
scope :in_namespace, ->(namespace) { where(namespace_id: namespace.id) }
scope :in_group_namespace, -> { joins(:group) }
- scope :sorted_by_activity, -> { reorder("projects.last_activity_at DESC") }
- scope :sorted_by_stars, -> { reorder("projects.star_count DESC") }
scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
- scope :joined, ->(user) { where("namespace_id != ?", user.namespace_id) }
+ scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
scope :public_only, -> { where(visibility_level: Project::PUBLIC) }
scope :public_and_internal_only, -> { where(visibility_level: Project.public_and_internal_levels) }
scope :non_archived, -> { where(archived: false) }
- enumerize :issues_tracker, in: (Gitlab.config.issues_tracker.keys).append(:gitlab), default: :gitlab
-
state_machine :import_status, initial: :none do
event :import_start do
- transition :none => :started
+ transition [:none, :finished] => :started
end
event :import_finish do
- transition :started => :finished
+ transition started: :finished
end
event :import_fail do
- transition :started => :failed
+ transition started: :failed
end
event :import_retry do
- transition :failed => :started
+ transition failed: :started
end
state :started
state :finished
state :failed
- after_transition any => :started, :do => :add_import_job
+ after_transition any => :started, do: :add_import_job
end
class << self
@@ -169,7 +198,7 @@ class Project < ActiveRecord::Base
def publicish(user)
visibility_levels = [Project::PUBLIC]
- visibility_levels += [Project::INTERNAL] if user
+ visibility_levels << Project::INTERNAL if user
where(visibility_level: visibility_levels)
end
@@ -178,21 +207,26 @@ class Project < ActiveRecord::Base
end
def active
- joins(:issues, :notes, :merge_requests).order("issues.created_at, notes.created_at, merge_requests.created_at DESC")
+ joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC')
end
def search(query)
- joins(:namespace).where("projects.archived = ?", false).where("projects.name LIKE :query OR projects.path LIKE :query OR namespaces.name LIKE :query OR projects.description LIKE :query", query: "%#{query}%")
+ joins(:namespace).where('projects.archived = ?', false).
+ where('LOWER(projects.name) LIKE :query OR
+ LOWER(projects.path) LIKE :query OR
+ LOWER(namespaces.name) LIKE :query OR
+ LOWER(projects.description) LIKE :query',
+ query: "%#{query.try(:downcase)}%")
end
def search_by_title(query)
- where("projects.archived = ?", false).where("LOWER(projects.name) LIKE :query", query: "%#{query.downcase}%")
+ where('projects.archived = ?', false).where('LOWER(projects.name) LIKE :query', query: "%#{query.downcase}%")
end
def find_with_namespace(id)
- return nil unless id.include?("/")
+ return nil unless id.include?('/')
- id = id.split("/")
+ id = id.split('/')
namespace = Namespace.find_by(path: id.first)
return nil unless namespace
@@ -204,13 +238,10 @@ class Project < ActiveRecord::Base
end
def sort(method)
- case method.to_s
- when 'newest' then reorder('projects.created_at DESC')
- when 'oldest' then reorder('projects.created_at ASC')
- when 'recently_updated' then reorder('projects.updated_at DESC')
- when 'last_updated' then reorder('projects.updated_at ASC')
- when 'largest_repository' then reorder('projects.repository_size DESC')
- else reorder("namespaces.path, projects.name ASC")
+ if method == 'repository_size_desc'
+ reorder(repository_size: :desc, id: :desc)
+ else
+ order_by(method)
end
end
end
@@ -260,19 +291,19 @@ class Project < ActiveRecord::Base
end
def to_param
- namespace.path + "/" + path
+ path
end
def web_url
- [gitlab_config.url, path_with_namespace].join("/")
+ [gitlab_config.url, path_with_namespace].join('/')
end
def web_url_without_protocol
- web_url.split("://")[1]
+ web_url.split('://')[1]
end
def build_commit_note(commit)
- notes.new(commit_id: commit.id, noteable_type: "Commit")
+ notes.new(commit_id: commit.id, noteable_type: 'Commit')
end
def last_activity
@@ -288,33 +319,68 @@ class Project < ActiveRecord::Base
end
def issue_exists?(issue_id)
- if used_default_issues_tracker?
+ if default_issues_tracker?
self.issues.where(iid: issue_id).first.present?
else
true
end
end
- def used_default_issues_tracker?
- self.issues_tracker == Project.issues_tracker.default_value
+ def default_issue_tracker
+ gitlab_issue_tracker_service || create_gitlab_issue_tracker_service
+ end
+
+ def issues_tracker
+ if external_issue_tracker
+ external_issue_tracker
+ else
+ default_issue_tracker
+ end
+ end
+
+ def default_issues_tracker?
+ if external_issue_tracker
+ false
+ else
+ true
+ end
+ end
+
+ def external_issues_trackers
+ services.select(&:issue_tracker?).reject(&:default?)
+ end
+
+ def external_issue_tracker
+ @external_issues_tracker ||= external_issues_trackers.select(&:activated?).first
end
def can_have_issues_tracker_id?
- self.issues_enabled && !self.used_default_issues_tracker?
+ self.issues_enabled && !self.default_issues_tracker?
end
def build_missing_services
- available_services_names.each do |service_name|
- service = services.find { |service| service.to_param == service_name }
+ services_templates = Service.where(template: true)
+
+ Service.available_services_names.each do |service_name|
+ service = find_service(services, service_name)
# If service is available but missing in db
- # we should create an instance. Ex `create_gitlab_ci_service`
- service = self.send :"create_#{service_name}_service" if service.nil?
+ if service.nil?
+ # We should check if template for the service exists
+ template = find_service(services_templates, service_name)
+
+ if template.nil?
+ # If no template, we should create an instance. Ex `create_gitlab_ci_service`
+ service = self.send :"create_#{service_name}_service"
+ else
+ Service.create_from_template(self.id, template)
+ end
+ end
end
end
- def available_services_names
- %w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla emails_on_push gemnasium slack pushover buildbox bamboo)
+ def find_service(list, name)
+ list.find { |service| service.to_param == name }
end
def gitlab_ci?
@@ -329,6 +395,19 @@ class Project < ActiveRecord::Base
@ci_service ||= ci_services.select(&:activated?).first
end
+ def avatar_type
+ unless self.avatar.image?
+ self.errors.add :avatar, 'only images allowed'
+ end
+ end
+
+ def avatar_in_git
+ @avatar_file ||= 'logo.png' if repository.blob_at_branch('master', 'logo.png')
+ @avatar_file ||= 'logo.jpg' if repository.blob_at_branch('master', 'logo.jpg')
+ @avatar_file ||= 'logo.gif' if repository.blob_at_branch('master', 'logo.gif')
+ @avatar_file
+ end
+
# For compatibility with old code
def code
path
@@ -356,7 +435,7 @@ class Project < ActiveRecord::Base
end
def team_member_by_name_or_email(name = nil, email = nil)
- user = users.where("name like ? or email like ?", name, email).first
+ user = users.where('name like ? or email like ?', name, email).first
project_members.where(user: user) if user
end
@@ -368,7 +447,7 @@ class Project < ActiveRecord::Base
def name_with_namespace
@name_with_namespace ||= begin
if namespace
- namespace.human_name + " / " + name
+ namespace.human_name + ' / ' + name
else
name
end
@@ -390,61 +469,20 @@ class Project < ActiveRecord::Base
end
def execute_services(data)
- services.each do |service|
-
- # Call service hook only if it is active
- begin
- service.execute(data) if service.active
- rescue => e
- logger.error(e)
- end
+ services.select(&:active).each do |service|
+ service.async_execute(data)
end
end
def update_merge_requests(oldrev, newrev, ref, user)
- return true unless ref =~ /heads/
- branch_name = ref.gsub("refs/heads/", "")
- commits = self.repository.commits_between(oldrev, newrev)
- c_ids = commits.map(&:id)
-
- # Close merge requests
- mrs = self.merge_requests.opened.where(target_branch: branch_name).to_a
- mrs = mrs.select(&:last_commit).select { |mr| c_ids.include?(mr.last_commit.id) }
-
- mrs.uniq.each do |merge_request|
- MergeRequests::MergeService.new.execute(merge_request, user, nil)
- end
-
- # Update code for merge requests into project between project branches
- mrs = self.merge_requests.opened.by_branch(branch_name).to_a
- # Update code for merge requests between project and project fork
- mrs += self.fork_merge_requests.opened.by_branch(branch_name).to_a
-
- mrs.uniq.each do |merge_request|
- merge_request.reload_code
- merge_request.mark_as_unchecked
- end
-
- # Add comment about pushing new commits to merge requests
- comment_mr_with_commits(branch_name, commits, user)
-
- true
- end
-
- def comment_mr_with_commits(branch_name, commits, user)
- mrs = self.origin_merge_requests.opened.where(source_branch: branch_name).to_a
- mrs += self.fork_merge_requests.opened.where(source_branch: branch_name).to_a
-
- mrs.uniq.each do |merge_request|
- Note.create_new_commits_note(merge_request, merge_request.project,
- user, commits)
- end
+ MergeRequests::RefreshService.new(self, user).
+ execute(oldrev, newrev, ref)
end
def valid_repo?
repository.exists?
rescue
- errors.add(:path, "Invalid repository path")
+ errors.add(:path, 'Invalid repository path')
false
end
@@ -503,7 +541,7 @@ class Project < ActiveRecord::Base
end
def http_url_to_repo
- [gitlab_config.url, "/", path_with_namespace, ".git"].join('')
+ [gitlab_config.url, '/', path_with_namespace, '.git'].join('')
end
# Check if current branch name is marked as protected in the system
@@ -511,6 +549,10 @@ class Project < ActiveRecord::Base
protected_branches_names.include?(branch_name)
end
+ def developers_can_push_to_protected_branch?(branch_name)
+ protected_branches.any? { |pb| pb.name == branch_name && pb.developers_can_push }
+ end
+
def forked?
!(forked_project_link.nil? || forked_project_link.forked_from_project.nil?)
end
@@ -562,6 +604,7 @@ class Project < ActiveRecord::Base
# Since we do cache @event we need to reset cache in special cases:
# * when project was moved
# * when project was renamed
+ # * when the project avatar changes
# Events cache stored like events/23-20130109142513.
# The cache key includes updated_at timestamp.
# Thus it will automatically generate a new fragment
@@ -621,4 +664,25 @@ class Project < ActiveRecord::Base
def origin_merge_requests
merge_requests.where(source_project_id: self.id)
end
+
+ def create_repository
+ if gitlab_shell.add_repository(path_with_namespace)
+ true
+ else
+ errors.add(:base, 'Failed to create repository')
+ false
+ end
+ end
+
+ def repository_exists?
+ !!repository.exists?
+ end
+
+ def create_wiki
+ ProjectWiki.new(self, self.owner).wiki
+ true
+ rescue ProjectWiki::CouldNotCreateWikiError => ex
+ errors.add(:base, 'Failed create wiki')
+ false
+ end
end
diff --git a/app/models/project_contributions.rb b/app/models/project_contributions.rb
new file mode 100644
index 00000000000..8ab2d814a94
--- /dev/null
+++ b/app/models/project_contributions.rb
@@ -0,0 +1,23 @@
+class ProjectContributions
+ attr_reader :project, :user
+
+ def initialize(project, user)
+ @project, @user = project, user
+ end
+
+ def commits_log
+ repository = project.repository
+
+ if !repository.exists? || repository.empty?
+ return {}
+ end
+
+ Rails.cache.fetch(cache_key) do
+ repository.commits_per_day_for_user(user)
+ end
+ end
+
+ def cache_key
+ "#{Date.today.to_s}-commits-log-#{project.id}-#{user.email}"
+ end
+end
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
new file mode 100644
index 00000000000..66b72572b9c
--- /dev/null
+++ b/app/models/project_services/asana_service.rb
@@ -0,0 +1,117 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+# template :boolean default(FALSE)
+#
+
+require 'asana'
+
+class AsanaService < Service
+ prop_accessor :api_key, :restrict_to_branch
+ validates :api_key, presence: true, if: :activated?
+
+ def title
+ 'Asana'
+ end
+
+ def description
+ 'Asana - Teamwork without email'
+ end
+
+ def help
+ 'This service adds commit messages as comments to Asana tasks.
+Once enabled, commit messages are checked for Asana task URLs
+(for example, `https://app.asana.com/0/123456/987654`) or task IDs
+starting with # (for example, `#987654`). Every task ID found will
+get the commit comment added to it.
+
+You can also close a task with a message containing: `fix #123456`.
+
+You can find your Api Keys here:
+http://developer.asana.com/documentation/#api_keys'
+ end
+
+ def to_param
+ 'asana'
+ end
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'api_key',
+ placeholder: 'User API token. User must have access to task,
+all comments will be attributed to this user.'
+ },
+ {
+ type: 'text',
+ name: 'restrict_to_branch',
+ placeholder: 'Comma-separated list of branches which will be
+automatically inspected. Leave blank to include all branches.'
+ }
+ ]
+ end
+
+ def execute(push)
+ Asana.configure do |client|
+ client.api_key = api_key
+ end
+
+ user = push[:user_name]
+ branch = push[:ref].gsub('refs/heads/', '')
+
+ branch_restriction = restrict_to_branch.to_s
+
+ # check the branch restriction is poplulated and branch is not included
+ if branch_restriction.length > 0 && branch_restriction.index(branch) == nil
+ return
+ end
+
+ project_name = project.name_with_namespace
+ push_msg = user + ' pushed to branch ' + branch + ' of ' + project_name
+
+ push[:commits].each do |commit|
+ check_commit(' ( ' + commit[:url] + ' ): ' + commit[:message], push_msg)
+ end
+ end
+
+ def check_commit(message, push_msg)
+ task_list = []
+ close_list = []
+
+ message.split("\n").each do |line|
+ # look for a task ID or a full Asana url
+ task_list.concat(line.scan(/#(\d+)/))
+ task_list.concat(line.scan(/https:\/\/app\.asana\.com\/\d+\/\d+\/(\d+)/))
+ # look for a word starting with 'fix' followed by a task ID
+ close_list.concat(line.scan(/(fix\w*)\W*#(\d+)/i))
+ end
+
+ # post commit to every taskid found
+ task_list.each do |taskid|
+ task = Asana::Task.find(taskid[0])
+
+ if task
+ task.create_story(text: push_msg + ' ' + message)
+ end
+ end
+
+ # close all tasks that had 'fix(ed/es/ing) #:id' in them
+ close_list.each do |taskid|
+ task = Asana::Task.find(taskid.last)
+
+ if task
+ task.modify(completed: true)
+ end
+ end
+ end
+end
diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb
index 0b90a14f39c..cf7598f35eb 100644
--- a/app/models/project_services/assembla_service.rb
+++ b/app/models/project_services/assembla_service.rb
@@ -5,11 +5,12 @@
# id :integer not null, primary key
# type :string(255)
# title :string(255)
-# project_id :integer not null
+# project_id :integer
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
+# template :boolean default(FALSE)
#
class AssemblaService < Service
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index b9eec9ab21e..df68803152f 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -1,15 +1,36 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+# template :boolean default(FALSE)
+#
+
class BambooService < CiService
include HTTParty
prop_accessor :bamboo_url, :build_key, :username, :password
- validates :bamboo_url, presence: true,
- format: { with: URI::regexp }, if: :activated?
+ validates :bamboo_url,
+ presence: true,
+ format: { with: URI::regexp },
+ if: :activated?
validates :build_key, presence: true, if: :activated?
- validates :username, presence: true,
- if: ->(service) { service.password? }, if: :activated?
- validates :password, presence: true,
- if: ->(service) { service.username? }, if: :activated?
+ validates :username,
+ presence: true,
+ if: ->(service) { service.password? },
+ if: :activated?
+ validates :password,
+ presence: true,
+ if: ->(service) { service.username? },
+ if: :activated?
attr_accessor :response
diff --git a/app/models/project_services/buildbox_service.rb b/app/models/project_services/buildbox_service.rb
index 0ab67b79fe4..058c890ae45 100644
--- a/app/models/project_services/buildbox_service.rb
+++ b/app/models/project_services/buildbox_service.rb
@@ -5,13 +5,13 @@
# id :integer not null, primary key
# type :string(255)
# title :string(255)
-# project_id :integer not null
+# project_id :integer
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
+# template :boolean default(FALSE)
#
-
require "addressable/uri"
class BuildboxService < CiService
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
index 0736ddab99b..14b6b87a0b7 100644
--- a/app/models/project_services/campfire_service.rb
+++ b/app/models/project_services/campfire_service.rb
@@ -5,11 +5,12 @@
# id :integer not null, primary key
# type :string(255)
# title :string(255)
-# project_id :integer not null
+# project_id :integer
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
+# template :boolean default(FALSE)
#
class CampfireService < Service
@@ -60,9 +61,9 @@ class CampfireService < Service
message << "[#{project.name_with_namespace}] "
message << "#{push[:user_name]} "
- if before =~ /000000/
+ if before.include?('000000')
message << "pushed new branch #{ref} \n"
- elsif after =~ /000000/
+ elsif after.include?('000000')
message << "removed branch #{ref} \n"
else
message << "pushed #{push[:total_commits_count]} commits to #{ref}. "
diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb
index b1d5e49ede3..5a26c25b3c3 100644
--- a/app/models/project_services/ci_service.rb
+++ b/app/models/project_services/ci_service.rb
@@ -5,11 +5,12 @@
# id :integer not null, primary key
# type :string(255)
# title :string(255)
-# project_id :integer not null
+# project_id :integer
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
+# template :boolean default(FALSE)
#
# Base class for CI services
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
new file mode 100644
index 00000000000..b29d1c86881
--- /dev/null
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -0,0 +1,53 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+# template :boolean default(FALSE)
+#
+
+class CustomIssueTrackerService < IssueTrackerService
+
+ prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
+
+ def title
+ if self.properties && self.properties['title'].present?
+ self.properties['title']
+ else
+ 'Custom Issue Tracker'
+ end
+ end
+
+ def description
+ if self.properties && self.properties['description'].present?
+ self.properties['description']
+ else
+ 'Custom issue tracker'
+ end
+ end
+
+ def to_param
+ 'custom_issue_tracker'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'title', placeholder: title },
+ { type: 'text', name: 'description', placeholder: description },
+ { type: 'text', name: 'project_url', placeholder: 'Project url' },
+ { type: 'text', name: 'issues_url', placeholder: 'Issue url' },
+ { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url' }
+ ]
+ end
+
+ def initialize_properties
+ self.properties = {} if properties.nil?
+ end
+end
diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb
index b9071b98295..86693ad0c7e 100644
--- a/app/models/project_services/emails_on_push_service.rb
+++ b/app/models/project_services/emails_on_push_service.rb
@@ -5,11 +5,12 @@
# id :integer not null, primary key
# type :string(255)
# title :string(255)
-# project_id :integer not null
+# project_id :integer
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
+# template :boolean default(FALSE)
#
class EmailsOnPushService < Service
diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb
index 0020b4482e5..13e2dfceb1a 100644
--- a/app/models/project_services/flowdock_service.rb
+++ b/app/models/project_services/flowdock_service.rb
@@ -5,11 +5,12 @@
# id :integer not null, primary key
# type :string(255)
# title :string(255)
-# project_id :integer not null
+# project_id :integer
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
+# template :boolean default(FALSE)
#
require "flowdock-git-hook"
@@ -37,13 +38,12 @@ class FlowdockService < Service
end
def execute(push_data)
- repo_path = File.join(Gitlab.config.gitlab_shell.repos_path, "#{project.path_with_namespace}.git")
Flowdock::Git.post(
push_data[:ref],
push_data[:before],
push_data[:after],
token: token,
- repo: repo_path,
+ repo: project.repository.path_to_repo,
repo_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}",
commit_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/%s",
diff_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/compare/%s...%s",
diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb
index 6d2fc06a5d0..a2c87ae88f1 100644
--- a/app/models/project_services/gemnasium_service.rb
+++ b/app/models/project_services/gemnasium_service.rb
@@ -5,11 +5,12 @@
# id :integer not null, primary key
# type :string(255)
# title :string(255)
-# project_id :integer not null
+# project_id :integer
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
+# template :boolean default(FALSE)
#
require "gemnasium/gitlab_service"
@@ -38,14 +39,13 @@ class GemnasiumService < Service
end
def execute(push_data)
- repo_path = File.join(Gitlab.config.gitlab_shell.repos_path, "#{project.path_with_namespace}.git")
Gemnasium::GitlabService.execute(
ref: push_data[:ref],
before: push_data[:before],
after: push_data[:after],
token: token,
api_key: api_key,
- repo: repo_path
+ repo: project.repository.path_to_repo
)
end
end
diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb
index a897c4ab76b..f4b463e8199 100644
--- a/app/models/project_services/gitlab_ci_service.rb
+++ b/app/models/project_services/gitlab_ci_service.rb
@@ -5,11 +5,12 @@
# id :integer not null, primary key
# type :string(255)
# title :string(255)
-# project_id :integer not null
+# project_id :integer
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
+# template :boolean default(FALSE)
#
class GitlabCiService < CiService
@@ -28,7 +29,7 @@ class GitlabCiService < CiService
end
def commit_status_path(sha)
- project_url + "/builds/#{sha}/status.json?token=#{token}"
+ project_url + "/commits/#{sha}/status.json?token=#{token}"
end
def get_ci_build(sha)
@@ -55,7 +56,7 @@ class GitlabCiService < CiService
end
def build_page(sha)
- project_url + "/builds/#{sha}"
+ project_url + "/commits/#{sha}"
end
def builds_path
@@ -81,7 +82,7 @@ class GitlabCiService < CiService
def fields
[
{ type: 'text', name: 'token', placeholder: 'GitLab CI project specific token' },
- { type: 'text', name: 'project_url', placeholder: 'http://ci.gitlabhq.com/projects/3'}
+ { type: 'text', name: 'project_url', placeholder: 'http://ci.gitlabhq.com/projects/3' }
]
end
end
diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb
new file mode 100644
index 00000000000..05c048e4e45
--- /dev/null
+++ b/app/models/project_services/gitlab_issue_tracker_service.rb
@@ -0,0 +1,46 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+# template :boolean default(FALSE)
+#
+
+class GitlabIssueTrackerService < IssueTrackerService
+ include Rails.application.routes.url_helpers
+ prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
+
+
+ def default?
+ true
+ end
+
+ def to_param
+ 'gitlab'
+ end
+
+ def project_url
+ "#{gitlab_url}#{namespace_project_issues_path(project.namespace, project)}"
+ end
+
+ def new_issue_url
+ "#{gitlab_url}#{new_namespace_project_issue_path(namespace_id: project.namespace, project_id: project)}"
+ end
+
+ def issue_url(iid)
+ "#{gitlab_url}#{namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: iid)}"
+ end
+
+ private
+
+ def gitlab_url
+ Gitlab.config.gitlab.relative_url_root.chomp("/") if Gitlab.config.gitlab.relative_url_root
+ end
+end
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 4078938cdbb..003e06a4c80 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -5,21 +5,22 @@
# id :integer not null, primary key
# type :string(255)
# title :string(255)
-# project_id :integer not null
+# project_id :integer
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
+# template :boolean default(FALSE)
#
class HipchatService < Service
MAX_COMMITS = 3
- prop_accessor :token, :room
+ prop_accessor :token, :room, :server
validates :token, presence: true, if: :activated?
def title
- 'Hipchat'
+ 'HipChat'
end
def description
@@ -32,8 +33,10 @@ class HipchatService < Service
def fields
[
- { type: 'text', name: 'token', placeholder: '' },
- { type: 'text', name: 'room', placeholder: '' }
+ { type: 'text', name: 'token', placeholder: 'Room token' },
+ { type: 'text', name: 'room', placeholder: 'Room name or ID' },
+ { type: 'text', name: 'server',
+ placeholder: 'Leave blank for default. https://hipchat.example.com' }
]
end
@@ -44,7 +47,9 @@ class HipchatService < Service
private
def gate
- @gate ||= HipChat::Client.new(token)
+ options = { api_version: 'v2' }
+ options[:server_url] = server unless server.blank?
+ @gate ||= HipChat::Client.new(token, options)
end
def create_message(push)
@@ -54,12 +59,12 @@ class HipchatService < Service
message = ""
message << "#{push[:user_name]} "
- if before =~ /000000/
+ if before.include?('000000')
message << "pushed new branch <a href=\""\
"#{project.web_url}/commits/#{URI.escape(ref)}\">#{ref}</a>"\
" to <a href=\"#{project.web_url}\">"\
"#{project.name_with_namespace.gsub!(/\s/, "")}</a>\n"
- elsif after =~ /000000/
+ elsif after.include?('000000')
message << "removed branch #{ref} from <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/,'')}</a> \n"
else
message << "pushed to branch <a href=\""\
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
new file mode 100644
index 00000000000..c991a34ecdb
--- /dev/null
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -0,0 +1,114 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+# template :boolean default(FALSE)
+#
+
+class IssueTrackerService < Service
+
+ validates :project_url, :issues_url, :new_issue_url, presence: true, if: :activated?
+
+ def category
+ :issue_tracker
+ end
+
+ def default?
+ false
+ end
+
+ def project_url
+ # implement inside child
+ end
+
+ def issues_url
+ # implement inside child
+ end
+
+ def new_issue_url
+ # implement inside child
+ end
+
+ def issue_url(iid)
+ self.issues_url.gsub(':id', iid.to_s)
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'description', placeholder: description },
+ { type: 'text', name: 'project_url', placeholder: 'Project url' },
+ { type: 'text', name: 'issues_url', placeholder: 'Issue url' },
+ { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url' }
+ ]
+ end
+
+ def initialize_properties
+ if properties.nil?
+ if enabled_in_gitlab_config
+ self.properties = {
+ title: issues_tracker['title'],
+ project_url: set_project_url,
+ issues_url: issues_tracker['issues_url'],
+ new_issue_url: issues_tracker['new_issue_url']
+ }
+ else
+ self.properties = {}
+ end
+ end
+ end
+
+ def execute(data)
+ message = "#{self.type} was unable to reach #{self.project_url}. Check the url and try again."
+ result = false
+
+ begin
+ url = URI.parse(self.project_url)
+
+ if url.host && url.port
+ http = Net::HTTP.start(url.host, url.port, { open_timeout: 5, read_timeout: 5 })
+ response = http.head("/")
+
+ if response
+ message = "#{self.type} received response #{response.code} when attempting to connect to #{self.project_url}"
+ result = true
+ end
+ end
+ rescue Timeout::Error, SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED => error
+ message = "#{self.type} had an error when trying to connect to #{self.project_url}: #{error.message}"
+ end
+ Rails.logger.info(message)
+ result
+ end
+
+ private
+
+ def enabled_in_gitlab_config
+ Gitlab.config.issues_tracker &&
+ Gitlab.config.issues_tracker.values.any? &&
+ issues_tracker
+ end
+
+ def issues_tracker
+ Gitlab.config.issues_tracker[to_param]
+ end
+
+ def set_project_url
+ if self.project
+ id = self.project.issues_tracker_id
+
+ if id
+ issues_tracker['project_url'].gsub(":issues_tracker_id", id)
+ end
+ end
+
+ issues_tracker['project_url']
+ end
+end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
new file mode 100644
index 00000000000..4c056605ea8
--- /dev/null
+++ b/app/models/project_services/jira_service.rb
@@ -0,0 +1,53 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+# template :boolean default(FALSE)
+#
+
+class JiraService < IssueTrackerService
+ include Rails.application.routes.url_helpers
+
+ prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
+
+ def help
+ issue_tracker_link = help_page_path("integration", "external-issue-tracker")
+
+ line1 = "Setting `project_url`, `issues_url` and `new_issue_url` will "\
+ "allow a user to easily navigate to the Jira issue tracker. "\
+ "See the [integration doc](#{issue_tracker_link}) for details."
+
+ line2 = 'Support for referencing commits and automatic closing of Jira issues directly ' \
+ 'from GitLab is [available in GitLab EE.](http://doc.gitlab.com/ee/integration/jira.html)'
+
+ [line1, line2].join("\n\n")
+ end
+
+ def title
+ if self.properties && self.properties['title'].present?
+ self.properties['title']
+ else
+ 'JIRA'
+ end
+ end
+
+ def description
+ if self.properties && self.properties['description'].present?
+ self.properties['description']
+ else
+ 'Jira issue tracker'
+ end
+ end
+
+ def to_param
+ 'jira'
+ end
+end
diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb
index 09e114f9cca..287812c57a5 100644
--- a/app/models/project_services/pivotaltracker_service.rb
+++ b/app/models/project_services/pivotaltracker_service.rb
@@ -5,11 +5,12 @@
# id :integer not null, primary key
# type :string(255)
# title :string(255)
-# project_id :integer not null
+# project_id :integer
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
+# template :boolean default(FALSE)
#
class PivotaltrackerService < Service
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index f247fde7762..3a3af59390a 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -5,11 +5,12 @@
# id :integer not null, primary key
# type :string(255)
# title :string(255)
-# project_id :integer not null
+# project_id :integer
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
+# template :boolean default(FALSE)
#
class PushoverService < Service
@@ -80,9 +81,9 @@ class PushoverService < Service
before = push_data[:before]
after = push_data[:after]
- if before =~ /000000/
+ if before.include?('000000')
message = "#{push_data[:user_name]} pushed new branch \"#{ref}\"."
- elsif after =~ /000000/
+ elsif after.include?('000000')
message = "#{push_data[:user_name]} deleted branch \"#{ref}\"."
else
message = "#{push_data[:user_name]} push to branch \"#{ref}\"."
diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb
new file mode 100644
index 00000000000..e1dc10415e0
--- /dev/null
+++ b/app/models/project_services/redmine_service.rb
@@ -0,0 +1,39 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+# template :boolean default(FALSE)
+#
+
+class RedmineService < IssueTrackerService
+
+ prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
+
+ def title
+ if self.properties && self.properties['title'].present?
+ self.properties['title']
+ else
+ 'Redmine'
+ end
+ end
+
+ def description
+ if self.properties && self.properties['description'].present?
+ self.properties['description']
+ else
+ 'Redmine issue tracker'
+ end
+ end
+
+ def to_param
+ 'redmine'
+ end
+end
diff --git a/app/models/project_services/slack_message.rb b/app/models/project_services/slack_message.rb
index 28204e5ea60..6c6446db45f 100644
--- a/app/models/project_services/slack_message.rb
+++ b/app/models/project_services/slack_message.rb
@@ -1,6 +1,14 @@
require 'slack-notifier'
class SlackMessage
+ attr_reader :after
+ attr_reader :before
+ attr_reader :commits
+ attr_reader :project_name
+ attr_reader :project_url
+ attr_reader :ref
+ attr_reader :username
+
def initialize(params)
@after = params.fetch(:after)
@before = params.fetch(:before)
@@ -23,14 +31,6 @@ class SlackMessage
private
- attr_reader :after
- attr_reader :before
- attr_reader :commits
- attr_reader :project_name
- attr_reader :project_url
- attr_reader :ref
- attr_reader :username
-
def message
if new_branch?
new_branch_message
@@ -77,11 +77,11 @@ class SlackMessage
end
def new_branch?
- before =~ /000000/
+ before.include?('000000')
end
def removed_branch?
- after =~ /000000/
+ after.include?('000000')
end
def branch_url
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index 963f5440b6f..297d8bbb5d4 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -5,11 +5,12 @@
# id :integer not null, primary key
# type :string(255)
# title :string(255)
-# project_id :integer not null
+# project_id :integer
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
+# template :boolean default(FALSE)
#
class SlackService < Service
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
new file mode 100644
index 00000000000..c4b6ef5d9a9
--- /dev/null
+++ b/app/models/project_services/teamcity_service.rb
@@ -0,0 +1,134 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+# template :boolean default(FALSE)
+#
+
+class TeamcityService < CiService
+ include HTTParty
+
+ prop_accessor :teamcity_url, :build_type, :username, :password
+
+ validates :teamcity_url,
+ presence: true,
+ format: { with: URI::regexp }, if: :activated?
+ validates :build_type, presence: true, if: :activated?
+ validates :username,
+ presence: true,
+ if: ->(service) { service.password? }, if: :activated?
+ validates :password,
+ presence: true,
+ if: ->(service) { service.username? }, if: :activated?
+
+ attr_accessor :response
+
+ after_save :compose_service_hook, if: :activated?
+
+ def compose_service_hook
+ hook = service_hook || build_service_hook
+ hook.save
+ end
+
+ def title
+ 'JetBrains TeamCity CI'
+ end
+
+ def description
+ 'A continuous integration and build server'
+ end
+
+ def help
+ 'The build configuration in Teamcity must use the build format '\
+ 'number %build.vcs.number% '\
+ 'you will also want to configure monitoring of all branches so merge '\
+ 'requests build, that setting is in the vsc root advanced settings.'
+ end
+
+ def to_param
+ 'teamcity'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'teamcity_url',
+ placeholder: 'TeamCity root URL like https://teamcity.example.com' },
+ { type: 'text', name: 'build_type',
+ placeholder: 'Build configuration ID' },
+ { type: 'text', name: 'username',
+ placeholder: 'A user with permissions to trigger a manual build' },
+ { type: 'password', name: 'password' },
+ ]
+ end
+
+ def build_info(sha)
+ url = URI.parse("#{teamcity_url}/httpAuth/app/rest/builds/"\
+ "branch:unspecified:any,number:#{sha}")
+ auth = {
+ username: username,
+ password: password,
+ }
+ @response = HTTParty.get("#{url}", verify: false, basic_auth: auth)
+ end
+
+ def build_page(sha)
+ build_info(sha) if @response.nil? || !@response.code
+
+ if @response.code != 200
+ # If actual build link can't be determined,
+ # send user to build summary page.
+ "#{teamcity_url}/viewLog.html?buildTypeId=#{build_type}"
+ else
+ # If actual build link is available, go to build result page.
+ built_id = @response['build']['id']
+ "#{teamcity_url}/viewLog.html?buildId=#{built_id}"\
+ "&buildTypeId=#{build_type}"
+ end
+ end
+
+ def commit_status(sha)
+ build_info(sha) if @response.nil? || !@response.code
+ return :error unless @response.code == 200 || @response.code == 404
+
+ status = if @response.code == 404
+ 'Pending'
+ else
+ @response['build']['status']
+ end
+
+ if status.include?('SUCCESS')
+ 'success'
+ elsif status.include?('FAILURE')
+ 'failed'
+ elsif status.include?('Pending')
+ 'pending'
+ else
+ :error
+ end
+ end
+
+ def execute(data)
+ auth = {
+ username: username,
+ password: password,
+ }
+
+ branch = data[:ref]
+
+ self.class.post("#{teamcity_url}/httpAuth/app/rest/buildQueue",
+ body: "<build branchName=\"#{branch}\">"\
+ "<buildType id=\"#{build_type}\"/>"\
+ '</build>',
+ headers: { 'Content-type' => 'application/xml' },
+ basic_auth: auth
+ )
+ end
+end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 657ee23ae23..bc9c3ce58f6 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -160,7 +160,7 @@ class ProjectTeam
end
user_ids = project_members.pluck(:user_id)
- user_ids += group_members.pluck(:user_id) if group
+ user_ids.push(*group_members.pluck(:user_id)) if group
User.where(id: user_ids)
end
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 770a26ed894..55438bee245 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -5,7 +5,7 @@ class ProjectWiki
'Markdown' => :markdown,
'RDoc' => :rdoc,
'AsciiDoc' => :asciidoc
- }
+ } unless defined?(MARKUPS)
class CouldNotCreateWikiError < StandardError; end
@@ -136,7 +136,7 @@ class ProjectWiki
def commit_details(action, message = nil, title = nil)
commit_message = message || default_message(action, title)
- {email: @user.email, name: @user.name, message: commit_message}
+ { email: @user.email, name: @user.name, message: commit_message }
end
def default_message(action, title)
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 1b06dd77523..97207ba1272 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -2,11 +2,12 @@
#
# Table name: protected_branches
#
-# id :integer not null, primary key
-# project_id :integer not null
-# name :string(255) not null
-# created_at :datetime
-# updated_at :datetime
+# id :integer not null, primary key
+# project_id :integer not null
+# name :string(255) not null
+# created_at :datetime
+# updated_at :datetime
+# developers_can_push :boolean default(FALSE), not null
#
class ProtectedBranch < ActiveRecord::Base
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 93994123a90..bbf35f04bbc 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -30,7 +30,7 @@ class Repository
commit = Gitlab::Git::Commit.find(raw_repository, id)
commit = Commit.new(commit) if commit
commit
- rescue Rugged::OdbError => ex
+ rescue Rugged::OdbError
nil
end
@@ -61,25 +61,25 @@ class Repository
end
def add_branch(branch_name, ref)
- Rails.cache.delete(cache_key(:branch_names))
+ cache.expire(:branch_names)
gitlab_shell.add_branch(path_with_namespace, branch_name, ref)
end
def add_tag(tag_name, ref, message = nil)
- Rails.cache.delete(cache_key(:tag_names))
+ cache.expire(:tag_names)
gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message)
end
def rm_branch(branch_name)
- Rails.cache.delete(cache_key(:branch_names))
+ cache.expire(:branch_names)
gitlab_shell.rm_branch(path_with_namespace, branch_name)
end
def rm_tag(tag_name)
- Rails.cache.delete(cache_key(:tag_names))
+ cache.expire(:tag_names)
gitlab_shell.rm_tag(path_with_namespace, tag_name)
end
@@ -97,19 +97,15 @@ class Repository
end
def branch_names
- Rails.cache.fetch(cache_key(:branch_names)) do
- raw_repository.branch_names
- end
+ cache.fetch(:branch_names) { raw_repository.branch_names }
end
def tag_names
- Rails.cache.fetch(cache_key(:tag_names)) do
- raw_repository.tag_names
- end
+ cache.fetch(:tag_names) { raw_repository.tag_names }
end
def commit_count
- Rails.cache.fetch(cache_key(:commit_count)) do
+ cache.fetch(:commit_count) do
begin
raw_repository.commit_count(self.root_ref)
rescue
@@ -121,26 +117,21 @@ class Repository
# Return repo size in megabytes
# Cached in redis
def size
- Rails.cache.fetch(cache_key(:size)) do
- raw_repository.size
- end
+ cache.fetch(:size) { raw_repository.size }
end
def expire_cache
- Rails.cache.delete(cache_key(:size))
- Rails.cache.delete(cache_key(:branch_names))
- Rails.cache.delete(cache_key(:tag_names))
- Rails.cache.delete(cache_key(:commit_count))
- Rails.cache.delete(cache_key(:graph_log))
- Rails.cache.delete(cache_key(:readme))
- Rails.cache.delete(cache_key(:version))
- Rails.cache.delete(cache_key(:contribution_guide))
+ %i(size branch_names tag_names commit_count graph_log
+ readme version contribution_guide).each do |key|
+ cache.expire(key)
+ end
end
def graph_log
- Rails.cache.fetch(cache_key(:graph_log)) do
+ cache.fetch(:graph_log) do
commits = raw_repository.log(limit: 6000, skip_merges: true,
ref: root_ref)
+
commits.map do |rugged_commit|
commit = Gitlab::Git::Commit.new(rugged_commit)
@@ -148,14 +139,30 @@ class Repository
author_name: commit.author_name.force_encoding('UTF-8'),
author_email: commit.author_email.force_encoding('UTF-8'),
additions: commit.stats.additions,
- deletions: commit.stats.deletions
+ deletions: commit.stats.deletions,
}
end
end
end
- def cache_key(type)
- "#{type}:#{path_with_namespace}"
+ def timestamps_by_user_log(user)
+ args = %W(git log --author=#{user.email} --since=#{(Date.today - 1.year).to_s} --pretty=format:%cd --date=short)
+ dates = Gitlab::Popen.popen(args, path_to_repo).first.split("\n")
+
+ if dates.present?
+ dates
+ else
+ []
+ end
+ end
+
+ def commits_per_day_for_user(user)
+ timestamps_by_user_log(user).
+ group_by { |commit_date| commit_date }.
+ inject({}) do |hash, (timestamp_date, commits)|
+ hash[timestamp_date] = commits.count
+ hash
+ end
end
def method_missing(m, *args, &block)
@@ -177,13 +184,11 @@ class Repository
end
def readme
- Rails.cache.fetch(cache_key(:readme)) do
- tree(:head).readme
- end
+ cache.fetch(:readme) { tree(:head).readme }
end
def version
- Rails.cache.fetch(cache_key(:version)) do
+ cache.fetch(:version) do
tree(:head).blobs.find do |file|
file.name.downcase == 'version'
end
@@ -191,9 +196,7 @@ class Repository
end
def contribution_guide
- Rails.cache.fetch(cache_key(:contribution_guide)) do
- tree(:head).contribution_guide
- end
+ cache.fetch(:contribution_guide) { tree(:head).contribution_guide }
end
def head_commit
@@ -235,7 +238,7 @@ class Repository
end
def last_commit_for_path(sha, path)
- args = %W(git rev-list --max-count 1 #{sha} -- #{path})
+ args = %W(git rev-list --max-count=1 #{sha} -- #{path})
sha = Gitlab::Popen.popen(args, path_to_repo).first.strip
commit(sha)
end
@@ -312,4 +315,27 @@ class Repository
[]
end
end
+
+ def tag_names_contains(sha)
+ args = %W(git tag --contains #{sha})
+ names = Gitlab::Popen.popen(args, path_to_repo).first
+
+ if names.respond_to?(:split)
+ names = names.split("\n").map(&:strip)
+
+ names.each do |name|
+ name.slice! '* '
+ end
+
+ names
+ else
+ []
+ end
+ end
+
+ private
+
+ def cache
+ @cache ||= RepositoryCache.new(path_with_namespace)
+ end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index c489c1e96e1..f87d875c10a 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -5,16 +5,17 @@
# id :integer not null, primary key
# type :string(255)
# title :string(255)
-# project_id :integer not null
+# project_id :integer
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
-#
+# template :boolean default(FALSE)
# To add new service you should build a class inherited from Service
# and implement a set of methods
class Service < ActiveRecord::Base
+ include Sortable
serialize :properties, JSON
default_value_for :active, false
@@ -24,12 +25,18 @@ class Service < ActiveRecord::Base
belongs_to :project
has_one :service_hook
- validates :project_id, presence: true
+ validates :project_id, presence: true, unless: Proc.new { |service| service.template? }
+
+ scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
def activated?
active
end
+ def template?
+ template
+ end
+
def category
:common
end
@@ -82,4 +89,24 @@ class Service < ActiveRecord::Base
}
end
end
+
+ def async_execute(data)
+ Sidekiq::Client.enqueue(ProjectServiceWorker, id, data)
+ end
+
+ def issue_tracker?
+ self.category == :issue_tracker
+ end
+
+ def self.available_services_names
+ %w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla asana
+ emails_on_push gemnasium slack pushover buildbox bamboo teamcity jira redmine custom_issue_tracker)
+ end
+
+ def self.create_from_template(project_id, template)
+ service = template.dup
+ service.template = false
+ service.project_id = project_id
+ service if service.save
+ end
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index a47fbca3260..82c1ab94446 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -16,6 +16,7 @@
#
class Snippet < ActiveRecord::Base
+ include Sortable
include Linguist::BlobHelper
include Gitlab::VisibilityLevel
@@ -29,7 +30,11 @@ class Snippet < ActiveRecord::Base
validates :author, presence: true
validates :title, presence: true, length: { within: 0..255 }
- validates :file_name, presence: true, length: { within: 0..255 }
+ validates :file_name,
+ presence: true,
+ length: { within: 0..255 },
+ format: { with: Gitlab::Regex.path_regex,
+ message: Gitlab::Regex.path_regex_message }
validates :content, presence: true
validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values }
@@ -62,6 +67,10 @@ class Snippet < ActiveRecord::Base
file_name
end
+ def sanitized_file_name
+ file_name.gsub(/[^a-zA-Z0-9_\-\.]+/, '')
+ end
+
def mode
nil
end
@@ -72,7 +81,7 @@ class Snippet < ActiveRecord::Base
def visibility_level_field
visibility_level
- end
+ end
class << self
def search(query)
diff --git a/app/models/user.rb b/app/models/user.rb
index 154cc0f3e16..55768a351e3 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -26,8 +26,6 @@
# bio :string(255)
# failed_attempts :integer default(0)
# locked_at :datetime
-# extern_uid :string(255)
-# provider :string(255)
# username :string(255)
# can_create_group :boolean default(TRUE), not null
# can_create_team :boolean default(TRUE), not null
@@ -36,29 +34,36 @@
# notification_level :integer default(1), not null
# password_expires_at :datetime
# created_by_id :integer
-# last_credential_check_at :datetime
# avatar :string(255)
# confirmation_token :string(255)
# confirmed_at :datetime
# confirmation_sent_at :datetime
# unconfirmed_email :string(255)
# hide_no_ssh_key :boolean default(FALSE)
+# hide_no_password :boolean default(FALSE)
# website_url :string(255) default(""), not null
+# last_credential_check_at :datetime
+# github_access_token :string(255)
+# notification_email :string(255)
+# password_automatically_set :boolean default(FALSE)
+# bitbucket_access_token :string(255)
#
require 'carrierwave/orm/activerecord'
require 'file_size_validator'
class User < ActiveRecord::Base
+ include Sortable
include Gitlab::ConfigHelper
- extend Gitlab::ConfigHelper
include TokenAuthenticatable
+ extend Gitlab::ConfigHelper
+ include Gitlab::CurrentSettings
default_value_for :admin, false
default_value_for :can_create_group, gitlab_config.default_can_create_group
default_value_for :can_create_team, false
default_value_for :hide_no_ssh_key, false
- default_value_for :projects_limit, gitlab_config.default_projects_limit
+ default_value_for :hide_no_password, false
default_value_for :theme_id, gitlab_config.default_theme
devise :database_authenticatable, :lockable, :async,
@@ -79,6 +84,7 @@ class User < ActiveRecord::Base
# Profile
has_many :keys, dependent: :destroy
has_many :emails, dependent: :destroy
+ has_many :identities, dependent: :destroy
# Groups
has_many :members, dependent: :destroy
@@ -105,32 +111,38 @@ class User < ActiveRecord::Base
has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event"
has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue"
has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest"
+ has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy
#
# Validations
#
validates :name, presence: true
- validates :email, presence: true, email: {strict_mode: true}, uniqueness: true
+ validates :email, presence: true, email: { strict_mode: true }, uniqueness: true
+ validates :notification_email, presence: true, email: { strict_mode: true }
validates :bio, length: { maximum: 255 }, allow_blank: true
- validates :extern_uid, allow_blank: true, uniqueness: {scope: :provider}
- validates :projects_limit, presence: true, numericality: {greater_than_or_equal_to: 0}
- validates :username, presence: true, uniqueness: { case_sensitive: false },
- exclusion: { in: Gitlab::Blacklist.path },
- format: { with: Gitlab::Regex.username_regex,
- message: Gitlab::Regex.username_regex_message }
+ validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 }
+ validates :username,
+ presence: true,
+ uniqueness: { case_sensitive: false },
+ exclusion: { in: Gitlab::Blacklist.path },
+ format: { with: Gitlab::Regex.username_regex,
+ message: Gitlab::Regex.username_regex_message }
validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true
validate :namespace_uniq, if: ->(user) { user.username_changed? }
validate :avatar_type, if: ->(user) { user.avatar_changed? }
validate :unique_email, if: ->(user) { user.email_changed? }
- validates :avatar, file_size: { maximum: 100.kilobytes.to_i }
+ validate :owns_notification_email, if: ->(user) { user.notification_email_changed? }
+ validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
before_validation :generate_password, on: :create
before_validation :sanitize_attrs
+ before_validation :set_notification_email, if: ->(user) { user.email_changed? }
before_save :ensure_authentication_token
after_save :ensure_namespace_correct
+ after_initialize :set_projects_limit
after_create :post_create_hook
after_destroy :post_destroy_hook
@@ -167,18 +179,16 @@ class User < ActiveRecord::Base
end
end
- mount_uploader :avatar, AttachmentUploader
+ mount_uploader :avatar, AvatarUploader
# Scopes
scope :admins, -> { where(admin: true) }
scope :blocked, -> { with_state(:blocked) }
scope :active, -> { with_state(:active) }
- scope :alphabetically, -> { order('name ASC') }
scope :in_team, ->(team){ where(id: team.member_ids) }
scope :not_in_team, ->(team){ where('users.id NOT IN (:ids)', ids: team.member_ids) }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
- scope :ldap, -> { where('provider LIKE ?', 'ldap%') }
scope :potential_team_members, ->(team) { team.members.any? ? active.not_in_team(team) : active }
#
@@ -197,11 +207,10 @@ class User < ActiveRecord::Base
def sort(method)
case method.to_s
- when 'recent_sign_in' then reorder('users.last_sign_in_at DESC')
- when 'oldest_sign_in' then reorder('users.last_sign_in_at ASC')
- when 'recently_created' then reorder('users.created_at DESC')
- when 'late_created' then reorder('users.created_at ASC')
- else reorder("users.name ASC")
+ when 'recent_sign_in' then reorder(last_sign_in_at: :desc)
+ when 'oldest_sign_in' then reorder(last_sign_in_at: :asc)
+ else
+ order_by(method)
end
end
@@ -226,6 +235,11 @@ class User < ActiveRecord::Base
where("lower(name) LIKE :query OR lower(email) LIKE :query OR lower(username) LIKE :query", query: "%#{query.downcase}%")
end
+ def by_login(login)
+ where('lower(username) = :value OR lower(email) = :value',
+ value: login.to_s.downcase).first
+ end
+
def by_username_or_id(name_or_id)
where('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i).first
end
@@ -233,6 +247,22 @@ class User < ActiveRecord::Base
def build_user(attrs = {})
User.new(attrs)
end
+
+ def clean_username(username)
+ username.gsub!(/@.*\z/, "")
+ username.gsub!(/\.git\z/, "")
+ username.gsub!(/\A-/, "")
+ username.gsub!(/[^a-zA-Z0-9_\-\.]/, "")
+
+ counter = 0
+ base = username
+ while User.by_login(username).present? || Namespace.by_path(username).present?
+ counter += 1
+ username = "#{base}#{counter}"
+ end
+
+ username
+ end
end
#
@@ -264,7 +294,8 @@ class User < ActiveRecord::Base
def namespace_uniq
namespace_name = self.username
- if Namespace.find_by(path: namespace_name)
+ existing_namespace = Namespace.by_path(namespace_name)
+ if existing_namespace && existing_namespace != self.namespace
self.errors.add :username, "already exists"
end
end
@@ -279,11 +310,15 @@ class User < ActiveRecord::Base
self.errors.add(:email, 'has already been taken') if Email.exists?(email: self.email)
end
+ def owns_notification_email
+ self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email)
+ end
+
# Groups user has access to
def authorized_groups
@authorized_groups ||= begin
group_ids = (groups.pluck(:id) + authorized_projects.pluck(:namespace_id))
- Group.where(id: group_ids).order('namespaces.name ASC')
+ Group.where(id: group_ids)
end
end
@@ -292,9 +327,9 @@ class User < ActiveRecord::Base
def authorized_projects
@authorized_projects ||= begin
project_ids = personal_projects.pluck(:id)
- project_ids += groups_projects.pluck(:id)
- project_ids += projects.pluck(:id).uniq
- Project.where(id: project_ids).joins(:namespace).order('namespaces.name ASC')
+ project_ids.push(*groups_projects.pluck(:id))
+ project_ids.push(*projects.pluck(:id).uniq)
+ Project.where(id: project_ids)
end
end
@@ -317,6 +352,10 @@ class User < ActiveRecord::Base
keys.count == 0
end
+ def require_password?
+ password_automatically_set? && !ldap_user?
+ end
+
def can_change_username?
gitlab_config.username_changing_enabled
end
@@ -402,7 +441,11 @@ class User < ActiveRecord::Base
end
def ldap_user?
- extern_uid && provider.start_with?('ldap')
+ identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
+ end
+
+ def ldap_identity
+ @ldap_identity ||= identities.find_by(["provider LIKE ?", "ldap%"])
end
def accessible_deploy_keys
@@ -420,6 +463,19 @@ class User < ActiveRecord::Base
end
end
+ def set_notification_email
+ if self.notification_email.blank? || !self.all_emails.include?(self.notification_email)
+ self.notification_email = self.email
+ end
+ end
+
+ def set_projects_limit
+ connection_default_value_defined = new_record? && !projects_limit_changed?
+ return unless self.projects_limit.nil? || connection_default_value_defined
+
+ self.projects_limit = current_application_settings.default_projects_limit
+ end
+
def requires_ldap_check?
if !Gitlab.config.ldap.enabled
false
@@ -478,7 +534,7 @@ class User < ActiveRecord::Base
end
def temp_oauth_email?
- email =~ /\Atemp-email-for-oauth/
+ email.start_with?('temp-email-for-oauth')
end
def public_profile?
@@ -487,12 +543,24 @@ class User < ActiveRecord::Base
def avatar_url(size = nil)
if avatar.present?
- [gitlab_config.url, avatar.url].join("/")
+ [gitlab_config.url, avatar.url].join
else
GravatarService.new.execute(email, size)
end
end
+ def all_emails
+ [self.email, *self.emails.map(&:email)]
+ end
+
+ def hook_attrs
+ {
+ name: name,
+ username: username,
+ avatar_url: avatar_url
+ }
+ end
+
def ensure_namespace_correct
# Ensure user has namespace
self.create_namespace!(path: self.username, name: self.username) unless self.namespace
@@ -504,7 +572,7 @@ class User < ActiveRecord::Base
def post_create_hook
log_info("User \"#{self.name}\" (#{self.email}) was created")
- notification_service.new_user(self, @reset_token)
+ notification_service.new_user(self, @reset_token) if self.created_by_id
system_hook_service.execute_hooks_for(self, :create)
end
@@ -538,4 +606,29 @@ class User < ActiveRecord::Base
UsersStarProject.create!(project: project, user: self)
end
end
+
+ def manageable_namespaces
+ @manageable_namespaces ||=
+ begin
+ namespaces = []
+ namespaces << namespace
+ namespaces += owned_groups
+ namespaces += masters_groups
+ end
+ end
+
+ def oauth_authorized_tokens
+ Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil)
+ end
+
+ def contributed_projects_ids
+ Event.where(author_id: self).
+ where("created_at > ?", Time.now - 1.year).
+ where("action = :pushed OR (target_type = 'MergeRequest' AND action = :created)",
+ pushed: Event::PUSHED, created: Event::CREATED).
+ reorder(project_id: :desc).
+ select(:project_id).
+ uniq
+ .map(&:project_id)
+ end
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index b9ab6702c53..32981a0e664 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -43,7 +43,7 @@ class WikiPage
@attributes[:slug]
end
- alias :to_param :slug
+ alias_method :to_param, :slug
# The formatted title of this page.
def title