diff --git a/Gemfile b/Gemfile index fb9224fac..49c6d12a4 100644 --- a/Gemfile +++ b/Gemfile @@ -23,12 +23,12 @@ gem 'state_machine' gem 'grack', :git => 'git://github.com/rdblue/grack.git', :require => 'git_http' gem "grit", :git => 'git://github.com/warpc/grit.git' #, :path => '~/Sites/code/grit' gem 'charlock_holmes', '~> 0.6.9' #, :git => 'git://github.com/brianmario/charlock_holmes.git', :branch => 'bundle-icu' -gem 'github-linguist', '~> 2.2.1', :require => 'linguist' +gem 'github-linguist', '~> 2.3.4', :require => 'linguist' gem 'diff-display', '~> 0.0.1' # Wiki gem "gollum", '~> 2.1.3' -gem "redcarpet", "~> 2.1.1" +gem "redcarpet", '~> 2.2.2' gem 'creole' gem 'rdiscount' # gem 'org-ruby' @@ -53,6 +53,7 @@ gem 'rack-throttle' gem 'rest-client', '~> 1.6.6' gem 'attr_encrypted', '1.2.1' +gem "gemoji", "~> 1.2.1", require: 'emoji/railtie' group :assets do gem 'sass-rails', '~> 3.2.5' diff --git a/Gemfile.lock b/Gemfile.lock index c73f08a63..2f620a232 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -121,10 +121,11 @@ GEM railties (>= 3.0.0) ffi (1.0.11) fssm (0.2.10) - github-linguist (2.2.1) + gemoji (1.2.1) + github-linguist (2.3.4) charlock_holmes (~> 0.6.6) escape_utils (~> 0.2.3) - mime-types (~> 1.18) + mime-types (~> 1.19) pygments.rb (>= 0.2.13) github-markdown (0.5.3) github-markup (0.7.5) @@ -257,7 +258,7 @@ GEM rdiscount (2.0.7.1) rdoc (3.12.2) json (~> 1.4) - redcarpet (2.1.1) + redcarpet (2.2.2) redis (3.0.3) redis-namespace (1.2.1) redis (~> 3.0.0) @@ -391,7 +392,8 @@ DEPENDENCIES devise (~> 2.1.2) diff-display (~> 0.0.1) factory_girl_rails (~> 4.0.0) - github-linguist (~> 2.2.1) + gemoji (~> 1.2.1) + github-linguist (~> 2.3.4) gollum (~> 2.1.3) grack! grit! @@ -415,7 +417,7 @@ DEPENDENCIES rails3-generators rails3-jquery-autocomplete (~> 1.0.7) rdiscount - redcarpet (~> 2.1.1) + redcarpet (~> 2.2.2) redhillonrails_core! resque (~> 1.21.0) resque-status (~> 0.3.3) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 432f4a399..c908583cb 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -3,4 +3,4 @@ @import "design/git"; @import "design/common"; @import "design/custom"; -@import "design/build_lists_monitoring"; \ No newline at end of file +@import "design/build_lists_monitoring"; diff --git a/app/assets/stylesheets/design/custom.scss b/app/assets/stylesheets/design/custom.scss index 078a44535..894c7097e 100644 --- a/app/assets/stylesheets/design/custom.scss +++ b/app/assets/stylesheets/design/custom.scss @@ -1909,6 +1909,15 @@ table#myTable thead tr.search th form.button_to div input { } } +.activity .state { + float: right; + padding: 3px 10px; + margin: 3px 0; + font-size: 12px; + color: white; + border-radius: 2px; +} + article .activity .top { .created { @@ -1955,7 +1964,6 @@ article .activity .top { } } - #assigned-popup { z-index: 1001; position: absolute; @@ -2007,3 +2015,7 @@ article .activity .top { } } } + +.cm-s-default.md_and_cm p img { + max-width: 800px; +} diff --git a/app/controllers/projects/comments_controller.rb b/app/controllers/projects/comments_controller.rb index e1f7f1073..587d33c9a 100644 --- a/app/controllers/projects/comments_controller.rb +++ b/app/controllers/projects/comments_controller.rb @@ -53,7 +53,7 @@ class Projects::CommentsController < Projects::BaseController end def find_or_build_comment - @comment = params[:id].present? && Comment.find(params[:id]) || + @comment = params[:id].present? && Comment.where(:automatic => false).find(params[:id]) || current_user.comments.build(params[:comment]) {|c| c.commentable = @commentable; c.project = @project} end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index b4b03d4fd..0e11f2c38 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -38,9 +38,12 @@ class Projects::IssuesController < Projects::BaseController end def create - @assignee_uname = params[:assignee_uname] @issue.user_id = current_user.id + unless can?(:write, @project) + @issue.assignee_id = nil + @issue.labelings = [] + end if @issue.save @issue.subscribe_creator(current_user.id) flash[:notice] = I18n.t("flash.issue.saved") @@ -56,6 +59,12 @@ class Projects::IssuesController < Projects::BaseController end def update + unless can?(:write, @project) + params.delete :update_labels + [:assignee_id, :labelings, :labelings_attributes].each do |k| + params[:issue].delete k + end if params[:issue] + end @issue.labelings.destroy_all if params[:update_labels] if params[:issue] && status = params[:issue][:status] @issue.set_close(current_user) if status == 'closed' diff --git a/app/controllers/projects/pull_requests_controller.rb b/app/controllers/projects/pull_requests_controller.rb index cce92b69e..c1d5e4d45 100644 --- a/app/controllers/projects/pull_requests_controller.rb +++ b/app/controllers/projects/pull_requests_controller.rb @@ -38,7 +38,7 @@ class Projects::PullRequestsController < Projects::BaseController authorize! :read, to_project @pull = to_project.pull_requests.new pull_params - @pull.issue.assignee_id = (params[:issue] || {})[:assignee_id] + @pull.issue.assignee_id = (params[:issue] || {})[:assignee_id] if can?(:write, to_project) @pull.issue.user, @pull.issue.project, @pull.from_project = current_user, to_project, @project @pull.from_project_owner_uname = @pull.from_project.owner.uname @pull.from_project_name = @pull.from_project.name diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index d2498da18..afacec351 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -38,15 +38,6 @@ module ApplicationHelper end end - def markdown(text) - unless @redcarpet - html_options = {filter_html: true, hard_wrap: true, with_toc_data: true} - options = {no_intraemphasis: true, tables: true, fenced_code_blocks: true, autolink: true, strikethrough: true, lax_html_blocks: true} - @redcarpet = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new(html_options), options) - end - @redcarpet.render(text).html_safe - end - def local_alert(text, type = 'error') html = "
#{text}" html << link_to('×', '#', :class => 'close close-alert', 'data-dismiss' => 'alert') diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb new file mode 100644 index 000000000..6a9ad45e9 --- /dev/null +++ b/app/helpers/gitlab_markdown_helper.rb @@ -0,0 +1,62 @@ +# This module is based on +# https://github.com/gitlabhq/gitlabhq/blob/7665b1de7eed4addd7b94786c84e6674710e6377/app/helpers/gitlab_markdown_helper.rb +module GitlabMarkdownHelper + include Modules::Models::Markdown + + # Use this in places where you would normally use link_to(gfm(...), ...). + # + # It solves a problem occurring with nested links (i.e. + # "outer text gfm ref more outer text"). This will not be + # interpreted as intended. Browsers will parse something like + # "outer text gfm ref more outer text" (notice the last part is + # not linked any more). link_to_gfm corrects that. It wraps all parts to + # explicitly produce the correct linking behavior (i.e. + # "outer text gfm ref more outer text"). + def link_to_gfm(body, url, html_options = {}) + return "" if body.blank? + + escaped_body = if body =~ /^\.*?}m) do |match| + "#{match}#{link_to("", url, html_options)[0..-5]}" # "".length +1 + end + + link_to(gfm_body.html_safe, url, html_options) + end + + def markdown(text) + unless @markdown + gitlab_renderer = Redcarpet::Render::GitlabHTML.new(self, + # see https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch- + filter_html: true, + with_toc_data: true, + hard_wrap: true) + @markdown = Redcarpet::Markdown.new(gitlab_renderer, + # see https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use + no_intra_emphasis: true, + tables: true, + fenced_code_blocks: true, + autolink: true, + strikethrough: true, + lax_html_blocks: true, + space_after_headers: true, + superscript: true) + end + + @markdown.render(text).html_safe + end + + def render_wiki_content(wiki_page) + if wiki_page.format == :markdown + markdown(wiki_page.content) + else + wiki_page.formatted_content.html_safe + end + end +end diff --git a/app/helpers/pull_request_helper.rb b/app/helpers/pull_request_helper.rb index 0b9cc87d4..fe25b4b87 100644 --- a/app/helpers/pull_request_helper.rb +++ b/app/helpers/pull_request_helper.rb @@ -1,7 +1,7 @@ # -*- encoding : utf-8 -*- module PullRequestHelper def merge_activity comments, commits - common_comments, pull_comments = comments.partition {|c| c.data.blank?} + common_comments, pull_comments = comments.partition {|c| c.automatic || c.data.blank?} common_comments = common_comments.map{ |c| [c.created_at, c] } pull_comments = pull_comments.group_by(&:data).map{|data, c| [c.first.created_at, [data || {}, [c].flatten]]} commits = commits.map{ |c| [(c.committed_date || c.authored_date), c] } @@ -23,7 +23,7 @@ module PullRequestHelper end def pull_header pull - str = "#{t '.header'} #{t 'from'} \ + str = "#{t '.header'} #{t 'from'} \ #{show_ref pull, 'from'} \ #{t 'into'} \ #{show_ref pull, 'to'}" diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 1424c819f..37560e66a 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -11,7 +11,7 @@ module UsersHelper elsif subject.kind_of? Group image_path('ava-big.png') else - gravatar_url(subject.email, subject.avatar.styles[size].geometry.split('x').first) + gravatar_url(subject.email, User::AVATAR_SIZES[size]) end end diff --git a/app/models/activity_feed_observer.rb b/app/models/activity_feed_observer.rb index 4d48d1572..4f8672e73 100644 --- a/app/models/activity_feed_observer.rb +++ b/app/models/activity_feed_observer.rb @@ -43,6 +43,7 @@ class ActivityFeedObserver < ActiveRecord::Observer end when 'Comment' + return if record.automatic if record.issue_comment? subscribes = record.commentable.subscribes subscribes.each do |subscribe| @@ -67,15 +68,16 @@ class ActivityFeedObserver < ActiveRecord::Observer (subscribe.user.committer?(record.commentable) && subscribe.user.notifier.new_comment_commit_owner) ) UserMailer.new_comment_notification(record, subscribe.user).deliver end - ActivityFeed.create( - :user => subscribe.user, - :kind => 'new_comment_commit_notification', - :data => {:user_name => record.user.name, :user_email => record.user.email, :user_id => record.user_id, :comment_body => record.body, - :commit_message => record.commentable.message, :commit_id => record.commentable.id, - :project_id => record.project.id, :comment_id => record.id, :project_name => record.project.name, :project_owner => record.project.owner.uname} - ) + ActivityFeed.create( + :user => subscribe.user, + :kind => 'new_comment_commit_notification', + :data => {:user_name => record.user.name, :user_email => record.user.email, :user_id => record.user_id, :comment_body => record.body, + :commit_message => record.commentable.message, :commit_id => record.commentable.id, + :project_id => record.project.id, :comment_id => record.id, :project_name => record.project.name, :project_owner => record.project.owner.uname} + ) end end + Comment.create_link_on_issues_from_item(record) when 'GitHook' return unless record.project @@ -90,7 +92,6 @@ class ActivityFeedObserver < ActiveRecord::Observer :change_type => change_type, :project_owner => record.project.owner.uname} else if record.message # online update - #FIXME using oldrev is a hack (only for online edit). last_commits, commits = [[record.newrev, record.message]], [] else commits = record.project.repo.commits_between(record.oldrev, record.newrev) @@ -104,6 +105,7 @@ class ActivityFeedObserver < ActiveRecord::Observer commits = commits[0...-3] options.merge!({:other_commits_count => commits.count, :other_commits => "#{commits[0].sha[0..9]}...#{commits[-1].sha[0..9]}"}) end + Comment.create_link_on_issues_from_item(record, last_commits) if last_commits.count > 0 end options.merge!({:user_id => record.user.id, :user_name => record.user.name, :user_email => record.user.email}) if record.user @@ -170,7 +172,9 @@ class ActivityFeedObserver < ActiveRecord::Observer ) end end + when 'Comment' + # dont remove outdated issues link + Comment.create_link_on_issues_from_item(record) end end - end diff --git a/app/models/avatar.rb b/app/models/avatar.rb index 2fbefa5e8..e4e4a4b7a 100644 --- a/app/models/avatar.rb +++ b/app/models/avatar.rb @@ -3,13 +3,14 @@ class Avatar < ActiveRecord::Base self.abstract_class = true MAX_AVATAR_SIZE = 5.megabyte + AVATAR_SIZES = {:micro => 16, :small => 30, :medium => 40, :big => 81} - has_attached_file :avatar, :styles => - { :micro => { :geometry => "16x16#", :format => :jpg, :convert_options => '-strip -background white -flatten -quality 70'}, - :small => { :geometry => "30x30#", :format => :jpg, :convert_options => '-strip -background white -flatten -quality 70'}, - :medium => { :geometry => "40x40#", :format => :jpg, :convert_options => '-strip -background white -flatten -quality 70'}, - :big => { :geometry => "81x81#", :format => :jpg, :convert_options => '-strip -background white -flatten -quality 70'} - } + AVATAR_SIZES_HASH = {}.tap do |styles| + AVATAR_SIZES.each do |name, size| + styles[name] = { :geometry => "#{size}x#{size}#", :format => :jpg, :convert_options => '-strip -background white -flatten -quality 70'} + end + end + has_attached_file :avatar, :styles => AVATAR_SIZES_HASH validates_inclusion_of :avatar_file_size, :in => (0..MAX_AVATAR_SIZE), :allow_nil => true attr_accessible :avatar diff --git a/app/models/comment.rb b/app/models/comment.rb index 61a2d1200..636d9e170 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,5 +1,11 @@ # -*- encoding : utf-8 -*- class Comment < ActiveRecord::Base + # regexp take from http://code.google.com/p/concerto-platform/source/browse/v3/cms/lib/CodeMirror/mode/gfm/gfm.js?spec=svn861&r=861#71 + # User/Project#Num + # User#Num + # #Num + ISSUES_REGEX = /(?:[a-zA-Z0-9\-_]*\/)?(?:[a-zA-Z0-9\-_]*)?[#!][0-9]+/ + belongs_to :commentable, :polymorphic => true, :touch => true belongs_to :user belongs_to :project @@ -85,7 +91,7 @@ class Comment < ActiveRecord::Base end def pull_comment? - return true if commentable.is_a?(Issue) && commentable.pull_request.present? + commentable.is_a?(Issue) && commentable.pull_request.present? end def set_additional_data params @@ -129,6 +135,57 @@ class Comment < ActiveRecord::Base return true end + def self.create_link_on_issues_from_item item, commits = nil + linker = item.user + elements = if item.is_a? Comment + [[item, item.body]] + elsif item.is_a? GitHook + commits + end + current_ability = Ability.new(linker) + + elements.each do |element| + element[1].scan(ISSUES_REGEX).each do |hash| + delimiter = if hash.include? '!' + '!' + elsif hash.include? '#' + '#' + else + raise 'Unknown delimiter for the hash tag!' + end + issue = Issue.find_by_hash_tag hash, current_ability, item.project, delimiter + next unless issue + # dont create link to the same issue + next if item.respond_to?(:commentable) && issue == item.try(:commentable) + find_dup = {:automatic => true, :commentable_type => issue.class.name, :commentable_id => issue.id} + if item.is_a? GitHook + find_dup.merge! :created_from_commit_hash => element[0].hex + elsif item.commentable_type == 'Issue' + find_dup.merge! :created_from_issue_id => item.commentable_id + elsif item.commentable_type == 'Grit::Commit' + find_dup.merge! :created_from_commit_hash => item.commentable_id + end + next if Comment.exists? find_dup # dont create duplicate link to issue + + comment = linker.comments.new :body => 'automatic comment' + comment.commentable, comment.project, comment.automatic = issue, issue.project, true + comment.data = {:from_project_id => item.project.id} + if item.is_a? GitHook + next unless item.project.repo.commit element[0] + comment.created_from_commit_hash = element[0].hex + else + comment.data.merge! :comment_id => item.id + if item.commentable_type == 'Issue' + comment.created_from_issue_id = item.commentable_id + elsif item.commentable_type == 'Grit::Commit' + comment.created_from_commit_hash = item.commentable_id + end + end + comment.save + end + end + end + protected def subscribe_on_reply diff --git a/app/models/issue.rb b/app/models/issue.rb index 1d61be583..4e1ec3e1b 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -67,6 +67,17 @@ class Issue < ActiveRecord::Base recipients end + def self.find_by_hash_tag hash_tag, current_ability, project, delimiter = '#' + hash_tag =~ /([a-zA-Z0-9\-_]*\/)?([a-zA-Z0-9\-_]*)?#{delimiter}([0-9]+)/ + owner_uname = Regexp.last_match[1].presence || Regexp.last_match[2].presence || project.owner.uname + project_name = Regexp.last_match[1] ? Regexp.last_match[2] : project.name + serial_id = Regexp.last_match[3] + project = Project.find_by_owner_and_name(owner_uname.chomp('/'), project_name) + return nil unless project + return nil unless current_ability.can? :show, project + project.issues.where(:serial_id => serial_id).first + end + protected def set_serial_id diff --git a/app/models/pull_request.rb b/app/models/pull_request.rb index 47a2ff04d..9786b9dc0 100644 --- a/app/models/pull_request.rb +++ b/app/models/pull_request.rb @@ -181,7 +181,7 @@ class PullRequest < ActiveRecord::Base system 'git', 'remote', 'add', 'head', from_project.path end end - clean # Need testing + #clean # Need testing end Dir.chdir(path) do diff --git a/app/presenters/comment_presenter.rb b/app/presenters/comment_presenter.rb index ac9564c31..ddab23e53 100644 --- a/app/presenters/comment_presenter.rb +++ b/app/presenters/comment_presenter.rb @@ -1,15 +1,34 @@ # -*- encoding : utf-8 -*- class CommentPresenter < ApplicationPresenter + include PullRequestHelper attr_accessor :comment, :options - attr_reader :header, :image, :date, :caption, :content, :buttons + attr_reader :header, :image, :date, :caption, :content, :buttons, :is_reference_to_issue def initialize(comment, opts = {}) - @comment = comment - @user = comment.user - @options = opts + @is_reference_to_issue = !!(comment.automatic && comment.created_from_issue_id) # is it reference issue from another issue + @comment, @user, @options = comment, comment.user, opts - @content = @comment.body + unless @is_reference_to_issue + @content = @comment.body + else + issue = Issue.where(:id => comment.created_from_issue_id).first + @referenced_issue = issue.pull_request || issue + if issue && Comment.exists?(comment.data[:comment_id]) + title = if issue == opts[:commentable] + "#{issue.serial_id}" + elsif issue.project.owner == opts[:commentable].project.owner + "#{issue.project.name}##{issue.serial_id}" + else + "#{issue.project.name_with_owner}##{issue.serial_id}" + end + title = "#{title}:" + issue_link = project_issue_path(issue.project, issue) + @content = "#{title} #{issue.title}".html_safe + else + @content = t 'layout.comments.removed' + end + end end def expandable? @@ -17,7 +36,7 @@ class CommentPresenter < ApplicationPresenter end def buttons? - true + !@is_reference_to_issue # dont show for automatic comment end def content? @@ -28,24 +47,33 @@ class CommentPresenter < ApplicationPresenter false end + def issue_referenced_state? + @referenced_issue # show state of the existing referenced issue + end + def buttons project, commentable = options[:project], options[:commentable] path = helpers.project_commentable_comment_path(project, commentable, comment) - res = [link_to(t("layout.link"), "#{helpers.project_commentable_path(project, commentable)}##{comment_anchor}", :class => "#{@options[:in_discussion].present? ? 'in_discussion_' : ''}link_to_comment").html_safe] + res = [link_to(t('layout.link'), "#{helpers.project_commentable_path(project, commentable)}##{comment_anchor}", :class => "#{@options[:in_discussion].present? ? 'in_discussion_' : ''}link_to_comment").html_safe] if controller.can? :update, @comment - res << link_to(t("layout.edit"), path, :id => "comment-#{comment.id}", :class => "edit_comment").html_safe + res << link_to(t('layout.edit'), path, :id => "comment-#{comment.id}", :class => "edit_comment").html_safe end if controller.can? :destroy, @comment - res << link_to(t("layout.delete"), path, :method => "delete", - :confirm => t("layout.comments.confirm_delete")).html_safe + res << link_to(t('layout.delete'), path, :method => "delete", + :confirm => t('layout.comments.confirm_delete')).html_safe end res end def header - res = link_to "#{@user.uname} (#{@user.name})", user_path(@user.uname) - res += ' ' + t("layout.comments.has_commented") + user_link = link_to @user.fullname, user_path(@user.uname) + res = unless @is_reference_to_issue + "#{user_link} #{t 'layout.comments.has_commented'}" + else + t 'layout.comments.reference', :user => user_link + end + res.html_safe end def image @@ -74,4 +102,12 @@ class CommentPresenter < ApplicationPresenter "#{before}comment#{@comment.id}" end + def issue_referenced_state + if @referenced_issue.is_a? Issue + statuses = {'open' => 'success', 'closed' => 'important'} + content_tag :span, t("layout.issues.status.#{@referenced_issue.status}"), :class => "state label-bootstrap label-#{statuses[@referenced_issue.status]}" + else + pull_status_label @referenced_issue + end.html_safe + end end diff --git a/app/presenters/git_presenters/commit_as_message_presenter.rb b/app/presenters/git_presenters/commit_as_message_presenter.rb index 1d2604f55..2d611d04e 100644 --- a/app/presenters/git_presenters/commit_as_message_presenter.rb +++ b/app/presenters/git_presenters/commit_as_message_presenter.rb @@ -2,33 +2,54 @@ class GitPresenters::CommitAsMessagePresenter < ApplicationPresenter include CommitHelper - attr_accessor :commit, :options - attr_reader :header, :image, :date, :caption, :content, :expandable + attr_accessor :commit + attr_reader :header, :image, :date, :caption, :content, :expandable, :is_reference_to_issue, :committer def initialize(commit, opts = {}) - @commit = commit - @options = opts + comment = opts[:comment] + @is_reference_to_issue = !!comment # is it reference issue from commit + @project = if comment + Project.where(:id => opts[:comment].data[:from_project_id]).first + else + opts[:project] + end + if @project + commit = commit || @project.repo.commit(comment.created_from_commit_hash.to_s(16)) + + @committer = User.where(:email => commit.committer.email).first || commit.committer + @commit_hash = commit.id + @committed_date, @authored_date = commit.committed_date, commit.authored_date + @commit_message = commit.message + else + @committer = t('layout.commits.unknown_committer') + @commit_hash = comment.created_from_commit_hash + @committed_date = @authored_date = comment.created_at + @commit_message = t('layout.commits.deleted') + end prepare_message end def header - @header ||= if options[:project].present? - I18n.t("layout.messages.commits.header", - :committer => committer_link, :commit => commit_link, :project => options[:project].name) - end.html_safe + @header ||= if @is_reference_to_issue + I18n.t('layout.commits.reference', :committer => committer_link, :commit => commit_link) + elsif @project.present? + I18n.t('layout.messages.commits.header', + :committer => committer_link, :commit => commit_link, :project => @project.name) + end.html_safe end def image - c = committer - @image ||= if c.class == User - helpers.avatar_url(c, :medium) + @image ||= if committer.is_a? User + helpers.avatar_url(committer, :medium) + elsif committer.is_a? Grit::Actor + helpers.avatar_url_by_email(committer.email, :medium) else - helpers.avatar_url_by_email(c.email, :medium) + helpers.gravatar_url('Unknown', User::AVATAR_SIZES[:medium]) end end def date - @date ||= I18n.l(@commit.committed_date || @commit.authored_date, :format => :long) + @date ||= I18n.l(@committed_date || @authored_date, :format => :long) end def expandable? @@ -51,33 +72,39 @@ class GitPresenters::CommitAsMessagePresenter < ApplicationPresenter false end + def issue_referenced_state? + false + end + protected - def committer - @committer ||= User.where(:email => @commit.committer.email).first || @commit.committer + def committer_link + @committer_link ||= if committer.is_a? User + link_to committer.uname, user_path(committer) + elsif committer.is_a? Grit::Actor + mail_to committer.email, committer.name + else # unknown committer + committer end + end - def committer_link - @committer_link ||= if committer.is_a? User - link_to committer.uname, user_path(committer) - else - mail_to committer.email, committer.name - end + def commit_link + if @project + link_to shortest_hash_id(@commit_hash), commit_path(@project, @commit_hash) + else + shortest_hash_id(@commit_hash) end + end - def commit_link - link_to shortest_hash_id(@commit.id), commit_path(options[:project], @commit.id) + def prepare_message + (@caption, @content) = @commit_message.split("\n\n", 2) + @caption = 'empty message' unless @caption.present? + if @caption.length > 72 + tmp = '...' + @caption[69..-1] + @content = (@content.present?) ? tmp + @content : tmp + @caption = @caption[0..68] + '...' end - - def prepare_message - (@caption, @content) = @commit.message.split("\n\n", 2) - @caption = 'empty message' unless @caption.present? - if @caption.length > 72 - tmp = '...' + @caption[69..-1] - @content = (@content.present?) ? tmp + @content : tmp - @caption = @caption[0..68] + '...' - end # @content = @content.gsub("\n", "
").html_safe if @content - @content = simple_format(@content, {}, :sanitize => true).html_safe if @content - end + @content = simple_format(@content, {}, :sanitize => true).html_safe if @content + end end diff --git a/app/views/platforms/base/_sidebar.html.haml b/app/views/platforms/base/_sidebar.html.haml index b5a857be5..4edefc28a 100644 --- a/app/views/platforms/base/_sidebar.html.haml +++ b/app/views/platforms/base/_sidebar.html.haml @@ -13,7 +13,7 @@ - if can? :show, @platform %li{:class => (act == :index && contr == :maintainers) ? 'active' : nil} = link_to t("layout.platforms.maintainers"), platform_maintainers_path(@platform) - - if can? :show, @platform + - if @platform.main? && can?(:show, @platform) %li{:class => (contr == :mass_builds && [:index, :create].include?(act)) ? 'active' : ''} = link_to t("layout.platforms.mass_build"), platform_mass_builds_path(@platform) - if can? :read, @platform.products.build diff --git a/app/views/platforms/mass_builds/index.html.haml b/app/views/platforms/mass_builds/index.html.haml index d965430c2..c82ed00b6 100644 --- a/app/views/platforms/mass_builds/index.html.haml +++ b/app/views/platforms/mass_builds/index.html.haml @@ -1,7 +1,7 @@ = render 'platforms/base/submenu' = render 'platforms/base/sidebar' -= render 'form' if can? :edit, @platform += render 'form' if can? :create, @platform.mass_builds.new %table.tablesorter.unbordered{:cellpadding => "0", :cellspacing => "0"} %thead diff --git a/app/views/projects/comments/_comment.html.haml b/app/views/projects/comments/_comment.html.haml index 0c25b2ac3..88c0e9f78 100644 --- a/app/views/projects/comments/_comment.html.haml +++ b/app/views/projects/comments/_comment.html.haml @@ -1,10 +1,11 @@ - CommentPresenter.present(comment, data) do |presenter| = render 'shared/feed_message', :presenter => presenter -#open-comment.comment.hidden{:class => "comment-#{comment.id}"} - =render 'projects/comments/button_md_help' - %h3.tmargin0= t("layout.comments.edit_header") - = form_for comment, :url => project_commentable_comment_path(data[:project], data[:commentable], comment), :html => { :class => 'form edit_comment' } do |f| - = render "projects/comments/form", :f => f, :id => "#{data[:add_id]}edit_#{comment.id}" - .comment-left - =link_to t('layout.cancel'), '#', :id => "comment-#{comment.id}", :class => 'cancel_edit_comment button' - .both +-unless comment.automatic + #open-comment.comment.hidden{:class => "comment-#{comment.id}"} + =render 'projects/comments/button_md_help' + %h3.tmargin0= t("layout.comments.edit_header") + = form_for comment, :url => project_commentable_comment_path(data[:project], data[:commentable], comment), :html => { :class => 'form edit_comment' } do |f| + = render "projects/comments/form", :f => f, :id => "#{data[:add_id]}edit_#{comment.id}" + .comment-left + =link_to t('layout.cancel'), '#', :id => "comment-#{comment.id}", :class => 'cancel_edit_comment button' + .both diff --git a/app/views/projects/comments/_list.html.haml b/app/views/projects/comments/_list.html.haml index e63505aa7..f7edaff1d 100644 --- a/app/views/projects/comments/_list.html.haml +++ b/app/views/projects/comments/_list.html.haml @@ -2,5 +2,9 @@ .hr %h3#block-list= t("layout.comments.comments_header") - list.each do |comment| - = render 'projects/comments/comment', :comment => comment, :data => {:project => project, :commentable => commentable} + -unless comment.created_from_commit_hash + = render 'projects/comments/comment', :comment => comment, :data => {:project => project, :commentable => commentable} + -else + - GitPresenters::CommitAsMessagePresenter.present(nil, :comment => comment) do |presenter| + = render 'shared/feed_message', :presenter => presenter, :item_no => "-#{comment.id}" = render "projects/comments/markdown_help" diff --git a/app/views/projects/comments/_markdown_help.html.haml b/app/views/projects/comments/_markdown_help.html.haml index 28cf08dd8..3c9a27af4 100644 --- a/app/views/projects/comments/_markdown_help.html.haml +++ b/app/views/projects/comments/_markdown_help.html.haml @@ -17,23 +17,27 @@ _This will also be italic_ **This text will be bold** __This will also be bold__ + %p=link_to t('layout.comments.md_cheatsheet.emoji_header'), 'http://www.emoji-cheat-sheet.com/', :target => '_blank' + %pre + =":smile:" + =image_tag(image_path('emoji/smile.png'), :class => 'emoji', :title => 'smile', :alt => 'smile', :size => "20x20") + =" :+1:" + =image_tag(image_path('emoji/+1.png'), :class => 'emoji', :title => '+1', :alt => '+1', :size => "20x20") .col %h3=t 'layout.comments.md_cheatsheet.lists' - %p=t 'layout.comments.md_cheatsheet.unordered' + %p{:style => 'float:left;margin-left:20px;'}=t 'layout.comments.md_cheatsheet.unordered' + %p{:style => 'float:right;margin-right:40px;'}=t 'layout.comments.md_cheatsheet.ordered' + .both %pre :preserve - * Item 1 - * Item 2 - * Item 2a - * Item 2b - %p=t 'layout.comments.md_cheatsheet.ordered' + * Item 1 1. Item 1 + * Item 2 2. Item 2 + * Item 2a 3. Item 3 + * Item 2b * Item 3a + * Item 3b + %p=t 'layout.comments.md_cheatsheet.reference_format' %pre - :preserve - 1. Item 1 - 2. Item 2 - 3. Item 3 - * Item 3a - * Item 3b + =preserve t('layout.comments.md_cheatsheet.reference_format_example').html_safe .col %h3=t 'layout.comments.md_cheatsheet.miscellaneous' %p=t 'layout.comments.md_cheatsheet.images' @@ -84,3 +88,4 @@ :preserve I think you should use an `[addr]` element here instead. + .both diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index a0948aebb..d960bafa9 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -1,13 +1,14 @@ =render 'title_body', :f => f, :id => 'new' -.leftlist= t('activerecord.attributes.issue.assignee') + ':' -#assigned-container.rightlist - =render 'user_container', :user => @issue.assignee -.both -.leftlist= t('layout.issues.labels') + ':' -.rightlist - %span#flag-span.small-text= t('layout.issues.choose_labels_on_left') - #issue_labels -.both +- if can?(:write, @project) + .leftlist= t('activerecord.attributes.issue.assignee') + ':' + #assigned-container.rightlist + =render 'user_container', :user => @issue.assignee + .both + .leftlist= t('layout.issues.labels') + ':' + .rightlist + %span#flag-span.small-text= t('layout.issues.choose_labels_on_left') + #issue_labels + .both .leftlist .rightlist %input{:type => "submit", :value => t(@issue.new_record? ? 'layout.create' : 'layout.update')} diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 2795d61c2..0154d5aa3 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -22,4 +22,4 @@ = link_to image_tag(avatar_url(issue.assignee), :alt => 'avatar', :class => 'avatar'), user_path(issue.assignee) if issue.assignee %a.answers{:href => "#{path}#block-list"} = image_tag 'answers.png', :class => 'pic' - .count= issue.comments.count \ No newline at end of file + .count=issue.comments.where(:automatic => false).count diff --git a/app/views/projects/issues/_manage_sidebar.html.haml b/app/views/projects/issues/_manage_sidebar.html.haml index 98144b2d2..1ba6fa829 100644 --- a/app/views/projects/issues/_manage_sidebar.html.haml +++ b/app/views/projects/issues/_manage_sidebar.html.haml @@ -1,8 +1,8 @@ -content_for :sidebar do - - can_manage = can?(:update, @issue) && @issue.persisted? || @issue.new_record? - if @issue.persisted? .bordered.nopadding %h3=t('activerecord.attributes.issue.status') + - can_manage = can?(:update, @issue) #switcher.issue_status{:class => "#{@issue.closed? ? 'switcher-off' : 'switcher'} #{can_manage ? "switch_issue_status" : ''}"} .swleft=t('layout.issues.status.open') .swright=t('layout.issues.status.closed') @@ -11,7 +11,7 @@ =hidden_field_tag "issue_status", @issue.closed? ? 'closed' : 'open', :name => "issue[status]" .block %h3=t('layout.issues.labels') - - if can_manage + - if can?(:write, @project) .current_labels - (@project.labels || []).each do |label| - is_issue_label = @issue.labels.include? label @@ -29,6 +29,6 @@ .label.nopointer .labeltext.selected{:style => "background: ##{label.color};"}=label.name .both - - if can_manage && @issue.persisted? + - if can?(:write, @project) && @issue.persisted? =link_to(t('layout.issues.label_manage'), '#', :class => "button tmargin10 manage_labels") =link_to(t('layout.issues.done'), '#', :class => "button tmargin10 update_labels", :style => 'display:none') diff --git a/app/views/projects/issues/_user_container.html.haml b/app/views/projects/issues/_user_container.html.haml index 47b78037d..21c1ca3a6 100644 --- a/app/views/projects/issues/_user_container.html.haml +++ b/app/views/projects/issues/_user_container.html.haml @@ -6,5 +6,5 @@ %span= t('layout.issues.is_assigned') - else %span= t('layout.issues.no_one_is_assigned') - -if can?(:update, @issue) || @issue.new_record? + -if can?(:write, @project) %span.icon-share \ No newline at end of file diff --git a/app/views/projects/pull_requests/_activity.html.haml b/app/views/projects/pull_requests/_activity.html.haml index cbde0e05b..7141c49db 100644 --- a/app/views/projects/pull_requests/_activity.html.haml +++ b/app/views/projects/pull_requests/_activity.html.haml @@ -5,7 +5,11 @@ -if item.is_a? Comment =render 'projects/git/commits/commits_small', :commits => commits_queue if commits_queue.present? -commits_queue.clear - =render 'projects/comments/comment', :comment => item, :data => {:project => @project, :commentable => @commentable} + -unless item.created_from_commit_hash + = render 'projects/comments/comment', :comment => item, :data => {:project => @project, :commentable => @commentable} + -else + - GitPresenters::CommitAsMessagePresenter.present(nil, :comment => item) do |presenter| + = render 'shared/feed_message', :presenter => presenter, :item_no => "-#{item.id}" -elsif item.is_a? Grit::Commit -commits_queue << item -elsif item.is_a? Array @@ -22,4 +26,3 @@ =render 'projects/pull_requests/discussion_comments', :item => item, :diff_counter => diff_counter - diff_counter += 1 =render 'projects/git/commits/commits_small', :commits => commits_queue if commits_queue.present? - diff --git a/app/views/projects/pull_requests/new.html.haml b/app/views/projects/pull_requests/new.html.haml index a4d30a2d8..9b148e318 100644 --- a/app/views/projects/pull_requests/new.html.haml +++ b/app/views/projects/pull_requests/new.html.haml @@ -32,11 +32,11 @@ %div{:class => @pull.ready? ? 'notice' : 'alert'} =pull_status @pull .both - - .leftlist.big-list= t('activerecord.attributes.issue.assignee') + ':' - #assigned-container.rightlist - =render 'projects/issues/user_container', :user => @pull.assignee - .both + - if can?(:write, @pull.to_project) + .leftlist.big-list= t('activerecord.attributes.issue.assignee') + ':' + #assigned-container.rightlist + =render 'projects/issues/user_container', :user => @pull.assignee + .both .leftlist.big-list .rightlist =f.submit t('.submit'), :class => 'btn btn-primary disabled', 'data-loading-text' => t('layout.processing'), :id => 'create_pull' unless @pull.already? diff --git a/app/views/shared/_feed_message.html.haml b/app/views/shared/_feed_message.html.haml index 414b4cc9b..2caadaf11 100644 --- a/app/views/shared/_feed_message.html.haml +++ b/app/views/shared/_feed_message.html.haml @@ -17,7 +17,11 @@ .both .both - if presenter.content? - .fulltext{:class => "#{presenter.expandable? ? "hidden" : ''} #{presenter.caption? ? "" : "alone"}", - :id => presenter.expandable? ? "content-expand#{item_no}" : ''} - .cm-s-default.md_and_cm=markdown presenter.content - .both + %div + =presenter.issue_referenced_state if presenter.issue_referenced_state? + .fulltext{:class => "#{presenter.expandable? ? "hidden" : ''} #{presenter.caption? ? "" : "alone"}", + :id => presenter.expandable? ? "content-expand#{item_no}" : ''} + .md_and_cm{:class => presenter.is_reference_to_issue ? '' : 'cm-s-default'} + =presenter.is_reference_to_issue ? presenter.content : markdown(presenter.content) + .both + diff --git a/config/initializers/setup.rb b/config/initializers/setup.rb index bb9d9b5c0..39f5bf3f3 100644 --- a/config/initializers/setup.rb +++ b/config/initializers/setup.rb @@ -10,3 +10,6 @@ Rosa::Application.config.middleware.insert_after ::Rails::Rack::Logger, ::Grack: Rosa::Application.config.middleware.insert_before ::Grack::Handler, ::Grack::Auth Rosa::Application.config.action_mailer.default_url_options = { :host => APP_CONFIG['action_mailer_host'] } if APP_CONFIG['action_mailer_host'] + +# Workaround for https://github.com/github/gemoji/pull/18 +Rosa::Application.config.assets.paths << Emoji.images_path \ No newline at end of file diff --git a/config/locales/layout/commits.en.yml b/config/locales/layout/commits.en.yml index 33b5b42c3..359d0f4d8 100644 --- a/config/locales/layout/commits.en.yml +++ b/config/locales/layout/commits.en.yml @@ -6,4 +6,7 @@ en: pluralize: commit: commit commits: commits - commits2: commits \ No newline at end of file + commits2: commits + reference: "%{committer} referenced this issue from a commit %{commit}" + deleted: Commit has since been removed from the git-repository and is no longer available. + unknown_committer: Unknown \ No newline at end of file diff --git a/config/locales/layout/commits.ru.yml b/config/locales/layout/commits.ru.yml index 3e88b0da3..4f3f5d4a8 100644 --- a/config/locales/layout/commits.ru.yml +++ b/config/locales/layout/commits.ru.yml @@ -6,4 +6,7 @@ ru: pluralize: commit: коммит commits: коммита - commits2: коммитов \ No newline at end of file + commits2: коммитов + reference: "%{committer} ссылается на данную задачу в коммите %{commit}" + deleted: Коммит был удален и более недоступен в git-репозитории. + unknown_committer: Неизвестный \ No newline at end of file diff --git a/config/locales/models/comment.en.yml b/config/locales/models/comment.en.yml index 8f3e5a4a6..5def48633 100644 --- a/config/locales/models/comment.en.yml +++ b/config/locales/models/comment.en.yml @@ -25,6 +25,22 @@ en: syntax_highlighting: Syntax highlighting indent_code: indent your code 4 spaces inline_code: Inline code for comments + emoji_header: Emoji Cheat Sheet + reference_format: Reference Format + reference_format_example: | + for members: @abf + for issues: + #123 abf#123 abf/rosa-build#123 + for pull requests: + !123 abf!123 abf/rosa-build!123 + for commits: 123456 + + issues: Issues + pull_requests: Pull Requests + members: Members + commits: Commits + reference: "%{user} referenced this issue" + removed: Comment has since been removed and is no longer available. flash: comment: diff --git a/config/locales/models/comment.ru.yml b/config/locales/models/comment.ru.yml index f481d904c..e4c5b0c4c 100644 --- a/config/locales/models/comment.ru.yml +++ b/config/locales/models/comment.ru.yml @@ -25,6 +25,21 @@ ru: syntax_highlighting: Подсветка синтаксиса indent_code: Отступ кода на 4 пробела inline_code: Встроенный код в строке + emoji_header: Шпаргалка по Emoji + reference_format: Формат ссылок + reference_format_example: | + для участников: @abf + для задач: + #123 abf#123 abf/rosa-build#123 + для пул реквестов: + !123 abf!123 abf/rosa-build!123 + для коммитов: 123456 + issues: Задачи + pull_requests: Пул реквесты + members: Участники + commits: Коммиты + reference: "%{user} ссылается на данную задачу" + removed: Комментарий был удален и более недоступен. flash: comment: diff --git a/config/locales/models/issue.en.yml b/config/locales/models/issue.en.yml index c06dfd908..b9930d4e2 100644 --- a/config/locales/models/issue.en.yml +++ b/config/locales/models/issue.en.yml @@ -1,5 +1,7 @@ en: activerecord: + models: + issue: Issue attributes: issue: title: Name diff --git a/config/locales/models/issue.ru.yml b/config/locales/models/issue.ru.yml index 1e1e3c190..51bd8db5e 100644 --- a/config/locales/models/issue.ru.yml +++ b/config/locales/models/issue.ru.yml @@ -1,5 +1,7 @@ ru: activerecord: + models: + issue: Задача attributes: issue: title: Название diff --git a/config/locales/models/pull_request.en.yml b/config/locales/models/pull_request.en.yml index 156bf8192..a27122c72 100644 --- a/config/locales/models/pull_request.en.yml +++ b/config/locales/models/pull_request.en.yml @@ -45,6 +45,8 @@ en: save_error: Unable to save pull request activerecord: + models: + pull_request: Pull Request attributes: pull_requests: to_ref: Source diff --git a/config/locales/models/pull_request.ru.yml b/config/locales/models/pull_request.ru.yml index 26dd0f761..e67132b06 100644 --- a/config/locales/models/pull_request.ru.yml +++ b/config/locales/models/pull_request.ru.yml @@ -46,6 +46,8 @@ ru: save_error: Не удалось сохранить пул реквест activerecord: + models: + pull_request: Пул реквест attributes: pull_requests: to_ref: Приемник diff --git a/db/migrate/20130319172358_add_automatic_to_comments.rb b/db/migrate/20130319172358_add_automatic_to_comments.rb new file mode 100644 index 000000000..a3f2d24bc --- /dev/null +++ b/db/migrate/20130319172358_add_automatic_to_comments.rb @@ -0,0 +1,9 @@ +class AddAutomaticToComments < ActiveRecord::Migration + def change + add_column :comments, :automatic, :boolean, :default => false + + add_index :comments, :commentable_type + add_index :comments, :automatic + add_index :comments, :commentable_id + end +end diff --git a/db/migrate/20130326165628_add_add_info_to_comments.rb b/db/migrate/20130326165628_add_add_info_to_comments.rb new file mode 100644 index 000000000..5a88f8414 --- /dev/null +++ b/db/migrate/20130326165628_add_add_info_to_comments.rb @@ -0,0 +1,9 @@ +class AddAddInfoToComments < ActiveRecord::Migration + def change + add_column :comments, :created_from_commit_hash, :decimal, :precision => 50, :scale => 0 + add_column :comments, :created_from_issue_id, :integer + + add_index :comments, :created_from_issue_id + add_index :comments, :created_from_commit_hash + end +end diff --git a/lib/ext/rails/reserved_name_validator.rb b/lib/ext/rails/reserved_name_validator.rb index 7d578e6c4..408c8b05f 100644 --- a/lib/ext/rails/reserved_name_validator.rb +++ b/lib/ext/rails/reserved_name_validator.rb @@ -3,7 +3,7 @@ class ReservedNameValidator < ActiveModel::EachValidator about account add admin administrator api autocomplete_group_uname app apps archive archives auth blog - config connect contact create commit commits + config connect contact create commit commits dashboard delete direct_messages downloads edit email faq favorites feed feeds follow followers following @@ -12,18 +12,17 @@ class ReservedNameValidator < ActiveModel::EachValidator jobs login log-in log_in logout log-out log_out logs map maps - new + new none oauth oauth_clients openid privacy register remove replies rss root save search sessions settings signup sign-up sign_up signin sign-in sign_in signout sign-out sign_out sitemap ssl subscribe - teams terms test trends tree + teams terms test tour trends tree unfollow unsubscribe url user widget widgets wiki xfn xmpp - tour } def reserved_names diff --git a/lib/modules/models/markdown.rb b/lib/modules/models/markdown.rb new file mode 100644 index 000000000..9efb3eded --- /dev/null +++ b/lib/modules/models/markdown.rb @@ -0,0 +1,199 @@ +# This module is based on +# https://github.com/gitlabhq/gitlabhq/blob/397c3da9758c03a215a308c011f94261d9c61cfa/lib/gitlab/markdown.rb +module Modules + module Models + # Custom parser for GitLab-flavored Markdown + # + # It replaces references in the text with links to the appropriate items in + # GitLab. + # + # Supported reference formats are: + # * @foo for team members + # * for issues: + # * #123 + # * abf#123 + # * abf/rosa-build#123 + # * for pull requests: + # * !123 + # * abf!123 + # * abf/rosa-build!123 + # * 123456 for commits + # + # It also parses Emoji codes to insert images. See + # http://www.emoji-cheat-sheet.com/ for a list of the supported icons. + # + # Examples + # + # >> gfm("Hey @david, can you fix this?") + # => "Hey @david, can you fix this?" + # + # >> gfm("Commit 35d5f7c closes #1234") + # => "Commit 35d5f7c closes #1234" + # + # >> gfm(":trollface:") + # => "\":trollface:\" + module Markdown + include IssuesHelper + + attr_reader :html_options + + # Public: Parse the provided text with GitLab-Flavored Markdown + # + # text - the source text + # html_options - extra options for the reference links as given to link_to + # + # Note: reference links will only be generated if @project is set + def gfm(text, html_options = {}) + return text if text.nil? + + # Duplicate the string so we don't alter the original, then call to_str + # to cast it back to a String instead of a SafeBuffer. This is required + # for gsub calls to work as we need them to. + text = text.dup.to_str + + @html_options = html_options + + # Extract pre blocks so they are not altered + # from http://github.github.com/github-flavored-markdown/ + text.gsub!(%r{
.*?
|.*?}m) { |match| extract_piece(match) } + # Extract links with probably parsable hrefs + text.gsub!(%r{.*?}m) { |match| extract_piece(match) } + # Extract images with probably parsable src + text.gsub!(%r{}m) { |match| extract_piece(match) } + + # TODO: add popups with additional information + + text = parse(text) + + # Insert pre block extractions + text.gsub!(/\{gfm-extraction-(\h{32})\}/) do + insert_piece($1) + end + + sanitize text.html_safe, attributes: ActionView::Base.sanitized_allowed_attributes + %w(id class) + end + + private + + def extract_piece(text) + @extractions ||= {} + + md5 = Digest::MD5.hexdigest(text) + @extractions[md5] = text + "{gfm-extraction-#{md5}}" + end + + def insert_piece(id) + @extractions[id] + end + + # Private: Parses text for references and emoji + # + # text - Text to parse + # + # Note: reference links will only be generated if @project is set + # + # Returns parsed text + def parse(text) + parse_references(text) if @project + parse_emoji(text) + + text + end + + REFERENCE_PATTERN = %r{ + (?[\W\/])? # Prefix + ( # Reference + @(?[a-zA-Z][a-zA-Z0-9_\-\.]*) # User uname + |(?(?:[a-zA-Z0-9\-_]*\/)?(?:[a-zA-Z0-9\-_]*)?\#[0-9]+) # Issue ID + |(?(?:[a-zA-Z0-9\-_]*\/)?(?:[a-zA-Z0-9\-_]*)?\![0-9]+) # PR ID + |(?[\h]{6,40}) # Commit ID + ) + (?\W)? # Suffix + }x.freeze + + TYPES = [:user, :issue, :pull_request, :commit].freeze + + def parse_references(text) + # parse reference links + text.gsub!(REFERENCE_PATTERN) do |match| + prefix = $~[:prefix] + suffix = $~[:suffix] + type = TYPES.select{|t| !$~[t].nil?}.first + identifier = $~[type] + + # Avoid HTML entities + if prefix && suffix && prefix[0] == '&' && suffix[-1] == ';' + match + elsif ref_link = reference_link(type, identifier) + "#{prefix}#{ref_link}#{suffix}" + else + match + end + end + end + + EMOJI_PATTERN = %r{(:(\S+):)}.freeze + + def parse_emoji(text) + # parse emoji + text.gsub!(EMOJI_PATTERN) do |match| + if valid_emoji?($2) + image_tag(image_path("emoji/#{$2}.png"), class: 'emoji', title: $1, alt: $1, size: "20x20") + else + match + end + end + end + + # Private: Checks if an emoji icon exists in the image asset directory + # + # emoji - Identifier of the emoji as a string (e.g., "+1", "heart") + # + # Returns boolean + def valid_emoji?(emoji) + Emoji.names.include? emoji + end + + # Private: Dispatches to a dedicated processing method based on reference + # + # reference - Object reference ("@1234", "!567", etc.) + # identifier - Object identifier (Issue ID, SHA hash, etc.) + # + # Returns string rendered by the processing method + def reference_link(type, identifier) + send("reference_#{type}", identifier) + end + + def reference_user(identifier) + if member = @project.all_members.select {|u| u.uname == identifier} #.joins(:user).where(users: {uname: identifier}).first + link_to("@#{identifier}", user_path(identifier), html_options.merge(class: "gfm gfm-team_member #{html_options[:class]}")) if member + end + end + + def reference_issue(identifier) + if issue = Issue.find_by_hash_tag(identifier, current_ability, @project) + url = project_issue_path(issue.project.owner, issue.project.name, issue.serial_id) + title = "#{Issue.model_name.human}: #{issue.title}" + link_to(identifier, url, html_options.merge(title: title, class: "gfm gfm-issue #{html_options[:class]}")) + end + end + + def reference_pull_request(identifier) + issue = Issue.find_by_hash_tag(identifier, current_ability, @project, '!') + if pull_request = issue.pull_request + title = "#{PullRequest.model_name.human}: #{pull_request.title}" + link_to(identifier, project_pull_request_path(pull_request.to_project, pull_request), html_options.merge(title: title, class: "gfm gfm-pull_request #{html_options[:class]}")) + end + end + + def reference_commit(identifier) + if commit = @project.repo.commit(identifier) + link_to shortest_hash_id(@commit.id), commit_path(options[:project], @commit.id) + title = GitPresenters::CommitAsMessagePresenter.present(commit, :project => @project).caption + link_to(identifier, commit_path(@project, commit), html_options.merge(title: title, class: "gfm gfm-commit #{html_options[:class]}")) + end + end + end + end +end diff --git a/lib/redcarpet/render/gitlab_html.rb b/lib/redcarpet/render/gitlab_html.rb new file mode 100644 index 000000000..a3e3e6724 --- /dev/null +++ b/lib/redcarpet/render/gitlab_html.rb @@ -0,0 +1,37 @@ +# This class is based on +# https://github.com/gitlabhq/gitlabhq/blob/2bc78739a7aa9d7e5109281fc45dbd41a1a576d4/lib/gitlab/markdown.rb +class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML + + attr_reader :template + alias_method :h, :template + + def initialize(template, options = {}) + @template = template + @project = @template.instance_variable_get("@project") + super options + end + + def block_code(code, language) + # New lines are placed to fix an rendering issue + # with code wrapped inside

tag for next case: + # + # # Title kinda h1 + # + # ruby code here + # + code_class = "class=\"#{language.downcase}\"" if language.present? + <<-HTML + +
#{code}
+ + HTML + end + + def link(link, title, content) + h.link_to_gfm(content, link, title: title) + end + + def postprocess(full_document) + h.gfm(full_document) + end +end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 65873a0a3..d6a7dd000 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -2,13 +2,14 @@ require 'spec_helper' shared_context "issues controller" do - before(:each) do + before do stub_symlink_methods @project = FactoryGirl.create(:project) @issue_user = FactoryGirl.create(:user) @issue = FactoryGirl.create(:issue, :project_id => @project.id, :assignee_id => @issue_user.id) + @label = FactoryGirl.create(:label, :project_id => @project.id) @project_with_turned_off_issues = FactoryGirl.create(:project, :has_issues => false) @turned_of_issue = FactoryGirl.create(:issue, :project_id => @project_with_turned_off_issues.id, :assignee_id => @issue_user.id) @@ -20,10 +21,10 @@ shared_context "issues controller" do :owner_name => @project.owner.uname, :project_name => @project.name, :issue => { :title => "issue1", - :body => "issue body" - }, - :assignee_id => @issue_user.id, - :assignee_uname => @issue_user.uname + :body => "issue body", + :labelings_attributes => { @label.id => {:label_id => @label.id}}, + :assignee_id => @issue_user.id + } } @update_params = { @@ -56,9 +57,7 @@ shared_examples_for 'issue user with project reader rights' do get :index, :owner_name => @project.owner.uname, :project_name => @project.name response.should render_template(:index) end -end -shared_examples_for 'issue user with project writer rights' do it 'should be able to perform create action' do post :create, @create_params response.should redirect_to(project_issues_path(@project)) @@ -69,6 +68,30 @@ shared_examples_for 'issue user with project writer rights' do end end +shared_examples_for 'issue user with project writer rights' do + it 'should be able to perform index action on hidden project' do + @project.update_attributes(:visibility => 'hidden') + get :index, :owner_name => @project.owner.uname, :project_name => @project.name + response.should render_template(:index) + end + + it 'should create issue object into db' do + lambda{ post :create, @create_params }.should change{ Issue.count }.by(1) + end + + context 'perform create action' do + before { post :create, @create_params } + + it 'user should be assigned to issue' do + @project.issues.last.assignee_id.should_not be_nil + end + + it 'label should be attached to issue' do + @project.issues.last.labels.should have(1).item + end + end +end + shared_examples_for 'user with issue update rights' do it 'should be able to perform update action' do put :update, {:id => @issue.serial_id}.merge(@update_params) @@ -167,11 +190,22 @@ describe Projects::IssuesController do it_should_behave_like 'issue user with project guest rights' it_should_behave_like 'issue user with project reader rights' - it_should_behave_like 'issue user with project writer rights' it_should_behave_like 'user without issue update rights' it_should_behave_like 'project with issues turned off' it_should_behave_like 'user without issue destroy rights' + context 'perform create action' do + before { post :create, @create_params } + + it 'user should not be assigned to issue' do + @project.issues.last.assignee_id.should be_nil + end + + it 'label should not be attached to issue' do + @project.issues.last.labels.should have(:no).items + end + end + # it 'should not be able to perform create action on project' do # post :create, @create_params # response.should redirect_to(forbidden_path) diff --git a/spec/factories/label.rb b/spec/factories/label.rb new file mode 100644 index 000000000..b985a2d98 --- /dev/null +++ b/spec/factories/label.rb @@ -0,0 +1,8 @@ +# -*- encoding : utf-8 -*- +FactoryGirl.define do + factory :label do + name { FactoryGirl.generate(:string) } + color 'FFF' + association :project, :factory => :project + end +end \ No newline at end of file diff --git a/spec/factories/labeling.rb b/spec/factories/labeling.rb new file mode 100644 index 000000000..51d1f6d70 --- /dev/null +++ b/spec/factories/labeling.rb @@ -0,0 +1,7 @@ +# -*- encoding : utf-8 -*- +FactoryGirl.define do + factory :labeling do + association :project, :factory => :project + association :label, :factory => :label + end +end \ No newline at end of file diff --git a/spec/models/comment_for_commit_spec.rb b/spec/models/comment_for_commit_spec.rb index a3b113a4a..9972a699a 100644 --- a/spec/models/comment_for_commit_spec.rb +++ b/spec/models/comment_for_commit_spec.rb @@ -6,6 +6,10 @@ def create_comment user FactoryGirl.create(:comment, :user => user, :commentable => @commit, :project => @project) end +def create_comment_in_commit commit, project, body + FactoryGirl.create(:comment, :user => @user, :commentable => commit, :project => project, :body => body) +end + def set_comments_data_for_commit @ability = Ability.new(@user) @@ -263,5 +267,63 @@ describe Comment do should_not_send_email(commentor: @user) end end + + context 'automatic issue linking' do + before(:each) do + @same_name_project = FactoryGirl.create(:project, :name => @project.name) + @issue_in_same_name_project = FactoryGirl.create(:issue, :project => @same_name_project, :user => @same_name_project.owner) + @another_project = FactoryGirl.create(:project, :owner => @user) + @other_user_project = FactoryGirl.create(:project) + @issue = FactoryGirl.create(:issue, :project => @project, :user => @user) + @second_issue = FactoryGirl.create(:issue, :project => @project, :user => @user) + @issue_in_another_project = FactoryGirl.create(:issue, :project => @another_project, :user => @user) + @issue_in_other_user_project = FactoryGirl.create(:issue, :project => @other_user_project, :user => @other_user_project.owner) + end + + it 'should create automatic comment' do + create_comment_in_commit(@commit, @project, "test link to ##{@issue.serial_id}; [##{@second_issue.serial_id}]") + Comment.where(:automatic => true, :commentable_type => 'Issue', + :commentable_id => @second_issue.id, + :created_from_commit_hash => @commit.id.hex).count.should == 1 + end + + it 'should create automatic comment in the another project issue' do + body = "[#{@another_project.name_with_owner}##{@issue_in_another_project.serial_id}]" + create_comment_in_commit(@commit, @project, body) + Comment.where(:automatic => true, :commentable_type => 'Issue', + :commentable_id => @issue_in_another_project.id, + :created_from_commit_hash => @commit.id.hex).count.should == 1 + end + + it 'should create automatic comment in the same name project issue' do + body = "[#{@same_name_project.owner.uname}##{@issue_in_same_name_project.serial_id}]" + create_comment_in_commit(@commit, @project, body) + Comment.where(:automatic => true, :commentable_type => 'Issue', + :commentable_id => @issue_in_same_name_project.id, + :created_from_commit_hash => @commit.id.hex).count.should == 1 + end + + it 'should not create duplicate automatic comment' do + create_comment_in_commit(@commit, @project, "test link to [##{@second_issue.serial_id}]") + create_comment_in_commit(@commit, @project, "test duplicate link to [##{@second_issue.serial_id}]") + Comment.where(:automatic => true, :commentable_type => 'Issue', + :commentable_id => @second_issue.id, + :created_from_commit_hash => @commit.id.hex).count.should == 1 + end + + it 'should not create duplicate automatic comment from one' do + create_comment_in_commit(@commit, @project, "test link to [##{@second_issue.serial_id}]; ##{@second_issue.serial_id}") + Comment.where(:automatic => true, :commentable_type => 'Issue', + :commentable_id => @second_issue.id, + :created_from_commit_hash => @commit.id.hex).count.should == 1 + end + it 'should create two automatic comment' do + body = "test ##{@second_issue.serial_id}" + + " && [#{@another_project.name_with_owner}##{@issue_in_another_project.serial_id}]" + create_comment_in_commit(@commit, @project, body) + Comment.where(:automatic => true, + :created_from_commit_hash => @commit.id.hex).count.should == 2 + end + end end end diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index 93e1dfc35..ef67f810f 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -14,6 +14,10 @@ def set_commentable_data any_instance_of(Project, :versions => ['v1.0', 'v2.0']) end +def create_comment_in_issue issue, body + FactoryGirl.create(:comment, :user => issue.user, :commentable => issue, :project => issue.project, :body => body) +end + describe Comment do before { stub_symlink_methods } context 'for global admin user' do @@ -100,5 +104,69 @@ describe Comment do @comment.should_not allow_mass_assignment_of :project_id end end + + context 'automatic issue linking' do + before(:each) do + @same_name_project = FactoryGirl.create(:project, :name => @project.name) + @issue_in_same_name_project = FactoryGirl.create(:issue, :project => @same_name_project, :user => @same_name_project.owner) + @another_project = FactoryGirl.create(:project, :owner => @user) + @other_user_project = FactoryGirl.create(:project) + @issue = FactoryGirl.create(:issue, :project => @project, :user => @user) + @second_issue = FactoryGirl.create(:issue, :project => @project, :user => @user) + @issue_in_another_project = FactoryGirl.create(:issue, :project => @another_project, :user => @user) + @issue_in_other_user_project = FactoryGirl.create(:issue, :project => @other_user_project, :user => @other_user_project.owner) + end + + it 'should create automatic comment' do + create_comment_in_issue(@issue, "test link to ##{@issue.serial_id}; [##{@second_issue.serial_id}]") + Comment.where(:automatic => true, :commentable_type => 'Issue', + :commentable_id => @second_issue.id, + :created_from_issue_id => @issue.id).count.should == 1 + end + + it 'should not create automatic comment to the same issue' do + create_comment_in_issue(@issue, "test link to ##{@issue.serial_id}; [##{@second_issue.serial_id}]") + Comment.where(:automatic => true, + :created_from_issue_id => @issue.id).count.should == 1 + end + + it 'should create automatic comment in the another project issue' do + body = "[#{@another_project.name_with_owner}##{@issue_in_another_project.serial_id}]" + create_comment_in_issue(@issue, body) + Comment.where(:automatic => true, :commentable_type => 'Issue', + :commentable_id => @issue_in_another_project.id, + :created_from_issue_id => @issue.id).count.should == 1 + end + + it 'should create automatic comment in the same name project issue' do + body = "[#{@same_name_project.owner.uname}##{@issue_in_same_name_project.serial_id}]" + create_comment_in_issue(@issue, body) + Comment.where(:automatic => true, :commentable_type => 'Issue', + :commentable_id => @issue_in_same_name_project.id, + :created_from_issue_id => @issue.id).count.should == 1 + end + + it 'should not create duplicate automatic comment' do + create_comment_in_issue(@issue, "test link to [##{@second_issue.serial_id}]") + create_comment_in_issue(@issue, "test duplicate link to [##{@second_issue.serial_id}]") + Comment.where(:automatic => true, :commentable_type => 'Issue', + :commentable_id => @second_issue.id, + :created_from_issue_id => @issue.id).count.should == 1 + end + + it 'should not create duplicate automatic comment from one' do + create_comment_in_issue(@issue, "test link to [##{@second_issue.serial_id}]; ##{@second_issue.serial_id}") + Comment.where(:automatic => true, :commentable_type => 'Issue', + :commentable_id => @second_issue.id, + :created_from_issue_id => @issue.id).count.should == 1 + end + it 'should create two automatic comment' do + body = "test ##{@second_issue.serial_id}" + + " && [#{@another_project.name_with_owner}##{@issue_in_another_project.serial_id}]" + create_comment_in_issue(@issue, body) + Comment.where(:automatic => true, + :created_from_issue_id => @issue.id).count.should == 2 + end + end end end