Skip to content
Snippets Groups Projects
Commit c8a115c0 authored by ash wilson's avatar ash wilson Committed by Ash Wilson
Browse files

Link issues from comments and automatically close them

Any mention of Issues, MergeRequests, or Commits via GitLab-flavored markdown
references in descriptions, titles, or attached Notes creates a back-reference
Note that links to the original referencer. Furthermore, pushing commits with
commit messages that match a (configurable) regexp to a project's default
branch will close any issues mentioned by GFM in the matched closing phrase.
If accepting a merge request would close any Issues in this way, a banner is
appended to the merge request's main panel to indicate this.
parent 2b36dee6
No related branches found
No related tags found
2 merge requests!5081fixed command to update init script,!4507Linking objects from GFM references
Showing
with 355 additions and 32 deletions
v 6.1.0
- Link issues, merge requests, and commits when they reference each other with GFM
- Close issues automatically when pushing commits with a special message
v 6.0.0 v 6.0.0
- Feature: Replace teams with group membership - Feature: Replace teams with group membership
We introduce group membership in 6.0 as a replacement for teams. We introduce group membership in 6.0 as a replacement for teams.
Loading
@@ -54,7 +58,7 @@ v 5.4.0
Loading
@@ -54,7 +58,7 @@ v 5.4.0
- Notify mentioned users with email - Notify mentioned users with email
   
v 5.3.0 v 5.3.0
- Refactored services - Refactored services
- Campfire service added - Campfire service added
- HipChat service added - HipChat service added
- Fixed bug with LDAP + git over http - Fixed bug with LDAP + git over http
Loading
Loading
Loading
@@ -3,6 +3,7 @@ require 'gitlab/satellite/satellite'
Loading
@@ -3,6 +3,7 @@ require 'gitlab/satellite/satellite'
class Projects::MergeRequestsController < Projects::ApplicationController class Projects::MergeRequestsController < Projects::ApplicationController
before_filter :module_enabled before_filter :module_enabled
before_filter :merge_request, only: [:edit, :update, :show, :commits, :diffs, :automerge, :automerge_check, :ci_status] before_filter :merge_request, only: [:edit, :update, :show, :commits, :diffs, :automerge, :automerge_check, :ci_status]
before_filter :closes_issues, only: [:edit, :update, :show, :commits, :diffs]
before_filter :validates_merge_request, only: [:show, :diffs] before_filter :validates_merge_request, only: [:show, :diffs]
before_filter :define_show_vars, only: [:show, :diffs] before_filter :define_show_vars, only: [:show, :diffs]
   
Loading
@@ -135,6 +136,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
Loading
@@ -135,6 +136,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request ||= @project.merge_requests.find_by_iid!(params[:id]) @merge_request ||= @project.merge_requests.find_by_iid!(params[:id])
end end
   
def closes_issues
@closes_issues ||= @merge_request.closes_issues
end
def authorize_modify_merge_request! def authorize_modify_merge_request!
return render_404 unless can?(current_user, :modify_merge_request, @merge_request) return render_404 unless can?(current_user, :modify_merge_request, @merge_request)
end end
Loading
Loading
Loading
@@ -2,6 +2,9 @@ class Commit
Loading
@@ -2,6 +2,9 @@ class Commit
include ActiveModel::Conversion include ActiveModel::Conversion
include StaticModel include StaticModel
extend ActiveModel::Naming extend ActiveModel::Naming
include Mentionable
attr_mentionable :safe_message
   
# Safe amount of files with diffs in one commit to render # Safe amount of files with diffs in one commit to render
# Used to prevent 500 error on huge commits by suppressing diff # Used to prevent 500 error on huge commits by suppressing diff
Loading
@@ -65,6 +68,29 @@ class Commit
Loading
@@ -65,6 +68,29 @@ class Commit
end end
end end
   
# Regular expression that identifies commit message clauses that trigger issue closing.
def issue_closing_regex
@issue_closing_regex ||= Regexp.new(Gitlab.config.gitlab.issue_closing_pattern)
end
# Discover issues should be closed when this commit is pushed to a project's
# default branch.
def closes_issues project
md = issue_closing_regex.match(safe_message)
if md
extractor = Gitlab::ReferenceExtractor.new
extractor.analyze(md[0])
extractor.issues_for(project)
else
[]
end
end
# Mentionable override.
def gfm_reference
"commit #{sha[0..5]}"
end
def method_missing(m, *args, &block) def method_missing(m, *args, &block)
@raw.send(m, *args, &block) @raw.send(m, *args, &block)
end end
Loading
Loading
# == Mentionable concern # == Mentionable concern
# #
# Contains common functionality shared between Issues and Notes # Contains functionality related to objects that can mention Users, Issues, MergeRequests, or Commits by
# GFM references.
# #
# Used by Issue, Note # Used by Issue, Note, MergeRequest, and Commit.
# #
module Mentionable module Mentionable
extend ActiveSupport::Concern extend ActiveSupport::Concern
   
module ClassMethods
# Indicate which attributes of the Mentionable to search for GFM references.
def attr_mentionable *attrs
mentionable_attrs.concat(attrs.map(&:to_s))
end
# Accessor for attributes marked mentionable.
def mentionable_attrs
@mentionable_attrs ||= []
end
end
# Generate a GFM back-reference that will construct a link back to this Mentionable when rendered. Must
# be overridden if this model object can be referenced directly by GFM notation.
def gfm_reference
raise NotImplementedError.new("#{self.class} does not implement #gfm_reference")
end
# Construct a String that contains possible GFM references.
def mentionable_text
self.class.mentionable_attrs.map { |attr| send(attr) || '' }.join
end
# The GFM reference to this Mentionable, which shouldn't be included in its #references.
def local_reference
self
end
# Determine whether or not a cross-reference Note has already been created between this Mentionable and
# the specified target.
def has_mentioned? target
Note.cross_reference_exists?(target, local_reference)
end
def mentioned_users def mentioned_users
users = [] users = []
return users if mentionable_text.blank? return users if mentionable_text.blank?
Loading
@@ -24,14 +59,39 @@ module Mentionable
Loading
@@ -24,14 +59,39 @@ module Mentionable
users.uniq users.uniq
end end
   
def mentionable_text # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
if self.class == Issue def references p = project, text = mentionable_text
description return [] if text.blank?
elsif self.class == Note ext = Gitlab::ReferenceExtractor.new
note ext.analyze(text)
else (ext.issues_for(p) + ext.merge_requests_for(p) + ext.commits_for(p)).uniq - [local_reference]
nil end
# Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+.
def create_cross_references! p = project, a = author, without = []
refs = references(p) - without
refs.each do |ref|
Note.create_cross_reference_note(ref, local_reference, a, p)
end end
end end
   
# If the mentionable_text field is about to change, locate any *added* references and create cross references for
# them. Invoke from an observer's #before_save implementation.
def notice_added_references p = project, a = author
ch = changed_attributes
original, mentionable_changed = "", false
self.class.mentionable_attrs.each do |attr|
if ch[attr]
original << ch[attr]
mentionable_changed = true
end
end
# Only proceed if the saved changes actually include a chance to an attr_mentionable field.
return unless mentionable_changed
preexisting = references(p, original)
create_cross_references!(p, a, preexisting)
end
end end
Loading
@@ -32,6 +32,7 @@ class Issue < ActiveRecord::Base
Loading
@@ -32,6 +32,7 @@ class Issue < ActiveRecord::Base
attr_accessible :title, :assignee_id, :position, :description, attr_accessible :title, :assignee_id, :position, :description,
:milestone_id, :label_list, :author_id_of_changes, :milestone_id, :label_list, :author_id_of_changes,
:state_event :state_event
attr_mentionable :title, :description
   
acts_as_taggable_on :labels acts_as_taggable_on :labels
   
Loading
@@ -56,4 +57,10 @@ class Issue < ActiveRecord::Base
Loading
@@ -56,4 +57,10 @@ class Issue < ActiveRecord::Base
   
# Both open and reopened issues should be listed as opened # Both open and reopened issues should be listed as opened
scope :opened, -> { with_state(:opened, :reopened) } scope :opened, -> { with_state(:opened, :reopened) }
# Mentionable overrides.
def gfm_reference
"issue ##{iid}"
end
end end
Loading
@@ -31,7 +31,7 @@ class MergeRequest < ActiveRecord::Base
Loading
@@ -31,7 +31,7 @@ class MergeRequest < ActiveRecord::Base
belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project" belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project"
   
attr_accessible :title, :assignee_id, :source_project_id, :source_branch, :target_project_id, :target_branch, :milestone_id, :author_id_of_changes, :state_event attr_accessible :title, :assignee_id, :source_project_id, :source_branch, :target_project_id, :target_branch, :milestone_id, :author_id_of_changes, :state_event
attr_mentionable :title
   
attr_accessor :should_remove_source_branch attr_accessor :should_remove_source_branch
   
Loading
@@ -255,6 +255,20 @@ class MergeRequest < ActiveRecord::Base
Loading
@@ -255,6 +255,20 @@ class MergeRequest < ActiveRecord::Base
target_project target_project
end end
   
# Return the set of issues that will be closed if this merge request is accepted.
def closes_issues
if target_branch == project.default_branch
unmerged_commits.map { |c| c.closes_issues(project) }.flatten.uniq.sort_by(&:id)
else
[]
end
end
# Mentionable override.
def gfm_reference
"merge request !#{iid}"
end
private private
   
def dump_commits(commits) def dump_commits(commits)
Loading
Loading
Loading
@@ -24,6 +24,7 @@ class Note < ActiveRecord::Base
Loading
@@ -24,6 +24,7 @@ class Note < ActiveRecord::Base
   
attr_accessible :note, :noteable, :noteable_id, :noteable_type, :project_id, attr_accessible :note, :noteable, :noteable_id, :noteable_type, :project_id,
:attachment, :line_code, :commit_id :attachment, :line_code, :commit_id
attr_mentionable :note
   
belongs_to :project belongs_to :project
belongs_to :noteable, polymorphic: true belongs_to :noteable, polymorphic: true
Loading
@@ -54,15 +55,36 @@ class Note < ActiveRecord::Base
Loading
@@ -54,15 +55,36 @@ class Note < ActiveRecord::Base
serialize :st_diff serialize :st_diff
before_create :set_diff, if: ->(n) { n.line_code.present? } before_create :set_diff, if: ->(n) { n.line_code.present? }
   
def self.create_status_change_note(noteable, project, author, status) def self.create_status_change_note(noteable, project, author, status, source)
body = "_Status changed to #{status}#{' by ' + source.gfm_reference if source}_"
create({
noteable: noteable,
project: project,
author: author,
note: body,
system: true
}, without_protection: true)
end
# +noteable+ was referenced from +mentioner+, by including GFM in either +mentioner+'s description or an associated Note.
# Create a system Note associated with +noteable+ with a GFM back-reference to +mentioner+.
def self.create_cross_reference_note(noteable, mentioner, author, project)
create({ create({
noteable: noteable, noteable: noteable,
commit_id: (noteable.sha if noteable.respond_to? :sha),
project: project, project: project,
author: author, author: author,
note: "_Status changed to #{status}_" note: "_mentioned in #{mentioner.gfm_reference}_",
system: true
}, without_protection: true) }, without_protection: true)
end end
   
# Determine whether or not a cross-reference note already exists.
def self.cross_reference_exists?(noteable, mentioner)
where(noteable_id: noteable.id, system: true, note: "_mentioned in #{mentioner.gfm_reference}_").any?
end
def commit_author def commit_author
@commit_author ||= @commit_author ||=
project.users.find_by_email(noteable.author_email) || project.users.find_by_email(noteable.author_email) ||
Loading
@@ -191,6 +213,16 @@ class Note < ActiveRecord::Base
Loading
@@ -191,6 +213,16 @@ class Note < ActiveRecord::Base
for_issue? || (for_merge_request? && !for_diff_line?) for_issue? || (for_merge_request? && !for_diff_line?)
end end
   
# Mentionable override.
def gfm_reference
noteable.gfm_reference
end
# Mentionable override.
def local_reference
noteable
end
def noteable_type_name def noteable_type_name
if noteable_type.present? if noteable_type.present?
noteable_type.downcase noteable_type.downcase
Loading
Loading
Loading
@@ -18,6 +18,7 @@ class Repository
Loading
@@ -18,6 +18,7 @@ class Repository
end end
   
def commit(id = nil) def commit(id = nil)
return nil unless raw_repository
commit = Gitlab::Git::Commit.find(raw_repository, id) commit = Gitlab::Git::Commit.find(raw_repository, id)
commit = Commit.new(commit) if commit commit = Commit.new(commit) if commit
commit commit
Loading
Loading
Loading
@@ -5,8 +5,8 @@ class ActivityObserver < BaseObserver
Loading
@@ -5,8 +5,8 @@ class ActivityObserver < BaseObserver
event_author_id = record.author_id event_author_id = record.author_id
   
if record.kind_of?(Note) if record.kind_of?(Note)
# Skip system status notes like 'status changed to close' # Skip system notes, like status changes and cross-references.
return true if record.note.include?("_Status changed to ") return true if record.system?
   
# Skip wall notes to prevent spamming of dashboard # Skip wall notes to prevent spamming of dashboard
return true if record.noteable_type.blank? return true if record.noteable_type.blank?
Loading
Loading
Loading
@@ -10,4 +10,8 @@ class BaseObserver < ActiveRecord::Observer
Loading
@@ -10,4 +10,8 @@ class BaseObserver < ActiveRecord::Observer
def current_user def current_user
Thread.current[:current_user] Thread.current[:current_user]
end end
def current_commit
Thread.current[:current_commit]
end
end end
class IssueObserver < BaseObserver class IssueObserver < BaseObserver
def after_create(issue) def after_create(issue)
notification.new_issue(issue, current_user) notification.new_issue(issue, current_user)
issue.create_cross_references!(issue.project, current_user)
end end
   
def after_close(issue, transition) def after_close(issue, transition)
Loading
@@ -17,12 +19,14 @@ class IssueObserver < BaseObserver
Loading
@@ -17,12 +19,14 @@ class IssueObserver < BaseObserver
if issue.is_being_reassigned? if issue.is_being_reassigned?
notification.reassigned_issue(issue, current_user) notification.reassigned_issue(issue, current_user)
end end
issue.notice_added_references(issue.project, current_user)
end end
   
protected protected
   
# Create issue note with service comment like 'Status changed to closed' # Create issue note with service comment like 'Status changed to closed'
def create_note(issue) def create_note(issue)
Note.create_status_change_note(issue, issue.project, current_user, issue.state) Note.create_status_change_note(issue, issue.project, current_user, issue.state, current_commit)
end end
end end
Loading
@@ -7,11 +7,13 @@ class MergeRequestObserver < ActivityObserver
Loading
@@ -7,11 +7,13 @@ class MergeRequestObserver < ActivityObserver
end end
   
notification.new_merge_request(merge_request, current_user) notification.new_merge_request(merge_request, current_user)
merge_request.create_cross_references!(merge_request.project, current_user)
end end
   
def after_close(merge_request, transition) def after_close(merge_request, transition)
create_event(merge_request, Event::CLOSED) create_event(merge_request, Event::CLOSED)
Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state) Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil)
   
notification.close_mr(merge_request, current_user) notification.close_mr(merge_request, current_user)
end end
Loading
@@ -33,11 +35,13 @@ class MergeRequestObserver < ActivityObserver
Loading
@@ -33,11 +35,13 @@ class MergeRequestObserver < ActivityObserver
   
def after_reopen(merge_request, transition) def after_reopen(merge_request, transition)
create_event(merge_request, Event::REOPENED) create_event(merge_request, Event::REOPENED)
Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state) Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil)
end end
   
def after_update(merge_request) def after_update(merge_request)
notification.reassigned_merge_request(merge_request, current_user) if merge_request.is_being_reassigned? notification.reassigned_merge_request(merge_request, current_user) if merge_request.is_being_reassigned?
merge_request.notice_added_references(merge_request.project, current_user)
end end
   
def create_event(record, status) def create_event(record, status)
Loading
Loading
class NoteObserver < BaseObserver class NoteObserver < BaseObserver
def after_create(note) def after_create(note)
notification.new_note(note) notification.new_note(note)
unless note.system?
# Create a cross-reference note if this Note contains GFM that names an
# issue, merge request, or commit.
note.references.each do |mentioned|
Note.create_cross_reference_note(mentioned, note.noteable, note.author, note.project)
end
end
end
def after_update(note)
note.notice_added_references(note.project, current_user)
end end
end end
class GitPushService class GitPushService
attr_accessor :project, :user, :push_data attr_accessor :project, :user, :push_data, :push_commits
   
# This method will be called after each git update # This method will be called after each git update
# and only if the provided user and project is present in GitLab. # and only if the provided user and project is present in GitLab.
# #
# All callbacks for post receive action should be placed here. # All callbacks for post receive action should be placed here.
# #
# Now this method do next: # Next, this method:
# 1. Ensure project satellite exists # 1. Creates the push event
# 2. Update merge requests # 2. Ensures that the project satellite exists
# 3. Execute project web hooks # 3. Updates merge requests
# 4. Execute project services # 4. Recognizes cross-references from commit messages
# 5. Create Push Event # 5. Executes the project's web hooks
# 6. Executes the project's services
# #
def execute(project, user, oldrev, newrev, ref) def execute(project, user, oldrev, newrev, ref)
@project, @user = project, user @project, @user = project, user
   
# Collect data for this git push # Collect data for this git push
@push_commits = project.repository.commits_between(oldrev, newrev)
@push_data = post_receive_data(oldrev, newrev, ref) @push_data = post_receive_data(oldrev, newrev, ref)
   
create_push_event create_push_event
Loading
@@ -25,11 +27,27 @@ class GitPushService
Loading
@@ -25,11 +27,27 @@ class GitPushService
project.discover_default_branch project.discover_default_branch
project.repository.expire_cache project.repository.expire_cache
   
if push_to_branch?(ref, oldrev) if push_to_existing_branch?(ref, oldrev)
project.update_merge_requests(oldrev, newrev, ref, @user) project.update_merge_requests(oldrev, newrev, ref, @user)
process_commit_messages(ref)
project.execute_hooks(@push_data.dup) project.execute_hooks(@push_data.dup)
project.execute_services(@push_data.dup) project.execute_services(@push_data.dup)
end end
if push_to_new_branch?(ref, oldrev)
# Re-find the pushed commits.
if is_default_branch?(ref)
# Initial push to the default branch. Take the full history of that branch as "newly pushed".
@push_commits = project.repository.commits(newrev)
else
# Use the pushed commits that aren't reachable by the default branch
# as a heuristic. This may include more commits than are actually pushed, but
# that shouldn't matter because we check for existing cross-references later.
@push_commits = project.repository.commits_between(project.default_branch, newrev)
end
process_commit_messages(ref)
end
end end
   
# This method provide a sample data # This method provide a sample data
Loading
@@ -45,7 +63,7 @@ class GitPushService
Loading
@@ -45,7 +63,7 @@ class GitPushService
protected protected
   
def create_push_event def create_push_event
Event.create( Event.create!(
project: project, project: project,
action: Event::PUSHED, action: Event::PUSHED,
data: push_data, data: push_data,
Loading
@@ -53,6 +71,36 @@ class GitPushService
Loading
@@ -53,6 +71,36 @@ class GitPushService
) )
end end
   
# Extract any GFM references from the pushed commit messages. If the configured issue-closing regex is matched,
# close the referenced Issue. Create cross-reference Notes corresponding to any other referenced Mentionables.
def process_commit_messages ref
is_default_branch = is_default_branch?(ref)
@push_commits.each do |commit|
# Close issues if these commits were pushed to the project's default branch and the commit message matches the
# closing regex. Exclude any mentioned Issues from cross-referencing even if the commits are being pushed to
# a different branch.
issues_to_close = commit.closes_issues(project)
author = commit_user(commit)
if !issues_to_close.empty? && is_default_branch
Thread.current[:current_user] = author
Thread.current[:current_commit] = commit
issues_to_close.each { |i| i.close && i.save }
end
# Create cross-reference notes for any other references. Omit any issues that were referenced in an
# issue-closing phrase, or have already been mentioned from this commit (probably from this commit
# being pushed to a different branch).
refs = commit.references(project) - issues_to_close
refs.reject! { |r| commit.has_mentioned?(r) }
refs.each do |r|
Note.create_cross_reference_note(r, commit, author, project)
end
end
end
# Produce a hash of post-receive data # Produce a hash of post-receive data
# #
# data = { # data = {
Loading
@@ -72,8 +120,6 @@ class GitPushService
Loading
@@ -72,8 +120,6 @@ class GitPushService
# } # }
# #
def post_receive_data(oldrev, newrev, ref) def post_receive_data(oldrev, newrev, ref)
push_commits = project.repository.commits_between(oldrev, newrev)
# Total commits count # Total commits count
push_commits_count = push_commits.size push_commits_count = push_commits.size
   
Loading
@@ -116,10 +162,26 @@ class GitPushService
Loading
@@ -116,10 +162,26 @@ class GitPushService
data data
end end
   
def push_to_branch? ref, oldrev def push_to_existing_branch? ref, oldrev
ref_parts = ref.split('/') ref_parts = ref.split('/')
   
# Return if this is not a push to a branch (e.g. new commits) # Return if this is not a push to a branch (e.g. new commits)
!(ref_parts[1] !~ /heads/ || oldrev == "00000000000000000000000000000000") ref_parts[1] =~ /heads/ && oldrev != "0000000000000000000000000000000000000000"
end
def push_to_new_branch? ref, oldrev
ref_parts = ref.split('/')
ref_parts[1] =~ /heads/ && oldrev == "0000000000000000000000000000000000000000"
end
def is_default_branch? ref
ref == "refs/heads/#{project.default_branch}"
end
def commit_user commit
User.where(email: commit.author_email).first ||
User.where(name: commit.author_name).first ||
user
end end
end end
Loading
@@ -33,4 +33,10 @@
Loading
@@ -33,4 +33,10 @@
%i.icon-ok %i.icon-ok
Merged by #{link_to_member(@project, @merge_request.merge_event.author)} Merged by #{link_to_member(@project, @merge_request.merge_event.author)}
#{time_ago_in_words(@merge_request.merge_event.created_at)} ago. #{time_ago_in_words(@merge_request.merge_event.created_at)} ago.
- if !@closes_issues.empty? && @merge_request.opened?
.ui-box-bottom.alert-info
%span
%i.icon-ok
Accepting this merge request will close #{@closes_issues.size == 1 ? 'issue' : 'issues'}
= succeed '.' do
!= gfm(@closes_issues.map { |i| "##{i.iid}" }.to_sentence)
Loading
@@ -46,6 +46,11 @@ production: &base
Loading
@@ -46,6 +46,11 @@ production: &base
## Users management ## Users management
# signup_enabled: true # default: false - Account passwords are not sent via the email if signup is enabled. # signup_enabled: true # default: false - Account passwords are not sent via the email if signup is enabled.
   
## Automatic issue closing
# If a commit message matches this regular express, all issues referenced from the matched text will be closed
# if it's pushed to a project's default branch.
# issue_closing_pattern: "^([Cc]loses|[Ff]ixes) +#\d+"
## Default project features settings ## Default project features settings
default_projects_features: default_projects_features:
issues: true issues: true
Loading
Loading
Loading
@@ -68,6 +68,7 @@ rescue ArgumentError # no user configured
Loading
@@ -68,6 +68,7 @@ rescue ArgumentError # no user configured
end end
Settings.gitlab['signup_enabled'] ||= false Settings.gitlab['signup_enabled'] ||= false
Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil? Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil?
Settings.gitlab['issue_closing_pattern'] = '^([Cc]loses|[Ff]ixes) #(\d+)' if Settings.gitlab['issue_closing_pattern'].nil?
Settings.gitlab['default_projects_features'] ||= {} Settings.gitlab['default_projects_features'] ||= {}
Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil? Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil?
Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil? Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil?
Loading
Loading
class AddSystemToNotes < ActiveRecord::Migration
class Note < ActiveRecord::Base
end
def up
add_column :notes, :system, :boolean, default: false, null: false
Note.reset_column_information
Note.update_all(system: false)
Note.where("note like '_status changed to%'").update_all(system: true)
end
def down
remove_column :notes, :system
end
end
Loading
@@ -152,6 +152,7 @@ ActiveRecord::Schema.define(:version => 20130821090531) do
Loading
@@ -152,6 +152,7 @@ ActiveRecord::Schema.define(:version => 20130821090531) do
t.string "commit_id" t.string "commit_id"
t.integer "noteable_id" t.integer "noteable_id"
t.text "st_diff" t.text "st_diff"
t.boolean "system", :default => false, :null => false
end end
   
add_index "notes", ["author_id"], :name => "index_notes_on_author_id" add_index "notes", ["author_id"], :name => "index_notes_on_author_id"
Loading
Loading
module Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor
attr_accessor :users, :issues, :merge_requests, :snippets, :commits
include Markdown
def initialize
@users, @issues, @merge_requests, @snippets, @commits = [], [], [], [], []
end
def analyze string
parse_references(string.dup)
end
# Given a valid project, resolve the extracted identifiers of the requested type to
# model objects.
def users_for project
users.map do |identifier|
project.users.where(username: identifier).first
end.reject(&:nil?)
end
def issues_for project
issues.map do |identifier|
project.issues.where(iid: identifier).first
end.reject(&:nil?)
end
def merge_requests_for project
merge_requests.map do |identifier|
project.merge_requests.where(iid: identifier).first
end.reject(&:nil?)
end
def snippets_for project
snippets.map do |identifier|
project.snippets.where(id: identifier).first
end.reject(&:nil?)
end
def commits_for project
repo = project.repository
return [] if repo.nil?
commits.map do |identifier|
repo.commit(identifier)
end.reject(&:nil?)
end
private
def reference_link type, identifier
# Append identifier to the appropriate collection.
send("#{type}s") << identifier
end
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment