From 34657b821ae597de76ffd5a70d2b0b298dc270ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 15 Dec 2015 18:09:09 -0500 Subject: Add syntax highlighting to diff view. #3945 --- app/helpers/application_helper.rb | 6 +++++ app/helpers/blob_helper.rb | 29 +++++++++++++++++++---- app/views/projects/diffs/_parallel_view.html.haml | 8 ++++--- app/views/projects/diffs/_text_file.html.haml | 6 +++-- lib/rouge/lexers/gitlab_diff.rb | 20 ++++++++++++++++ spec/helpers/blob_helper_spec.rb | 11 +++++++++ 6 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 lib/rouge/lexers/gitlab_diff.rb diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 0b00b9a0702..bc4b6ec0327 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -326,4 +326,10 @@ module ApplicationHelper def truncate_first_line(message, length = 50) truncate(message.each_line.first.chomp, length: length) if message end + + def unescape_html(content) + text = CGI.unescapeHTML(content) + text.gsub!(' ', ' ') + text + end end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index d31d4cde08f..bf18673972c 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -1,11 +1,17 @@ module BlobHelper - def highlight(blob_name, blob_content, nowrap: false, continue: false) - @formatter ||= Rouge::Formatters::HTMLGitlab.new( - nowrap: nowrap, + def rouge_formatter(options = {}) + default_options = { + nowrap: false, cssclass: 'code highlight', lineanchors: true, lineanchorsid: 'LC' - ) + } + + Rouge::Formatters::HTMLGitlab.new(default_options.merge!(options)) + end + + def highlight(blob_name, blob_content, nowrap: false, continue: false) + @formatter ||= rouge_formatter(nowrap: nowrap) begin @lexer ||= Rouge::Lexer.guess(filename: blob_name, source: blob_content).new @@ -18,6 +24,21 @@ module BlobHelper result end + def highlight_line(blob_name, content, continue: false) + if @previous_blob_name != blob_name + @parent = Rouge::Lexer.guess(filename: blob_name, source: content).new rescue Rouge::Lexers::PlainText.new + @lexer = Rouge::Lexers::GitlabDiff.new(parent_lexer: @parent) + @options = Rouge::Lexers::PlainText === @parent ? {} : { continue: continue } + end + + @previous_blob_name = blob_name + @formatter ||= rouge_formatter(nowrap: true) + + content.sub!(/\A((?:\+|-)\s*)/, '') # Don't format '+' or '-' indicators. + + "#{$1}#{@formatter.format(@lexer.lex(content, @options))}".html_safe + end + def no_highlight_files %w(credits changelog news copying copyright license authors) end diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 37fd1b1ec8a..c6a9d71e789 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -1,5 +1,5 @@ / Side-by-side diff view -%div.text-file.diff-wrap-lines +%div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight{ class: user_color_scheme } %table - parallel_diff(diff_file, index).each do |line| - type_left = line[0] @@ -20,7 +20,8 @@ = link_to raw(line_number_left), "##{line_code_left}", id: line_code_left - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code_left, 'old') - %td.line_content{class: "parallel noteable_line #{type_left} #{line_code_left}", "line_code" => line_code_left }= raw line_content_left + %td.line_content{class: "parallel noteable_line #{type_left} #{line_code_left}", "line_code" => line_code_left }< + = highlight_line(diff_file.new_path, unescape_html(line_content_left)) - if type_right == 'new' - new_line_class = 'new' @@ -33,7 +34,8 @@ = link_to raw(line_number_right), "##{new_line_code}", id: new_line_code - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code_right, 'new') - %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", "line_code" => new_line_code}= raw line_content_right + %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", "line_code" => new_line_code}< + = highlight_line(diff_file.new_path, unescape_html(line_content_right)) - if @reply_allowed - comments_left, comments_right = organize_comments(type_left, type_right, line_code_left, line_code_right) diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index 977ca423f75..78c66a6291e 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -3,7 +3,8 @@ .suppressed-container %a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show. -%table.text-file{class: "#{'hide' if too_big}"} +%table.text-file.code.js-syntax-highlight{ class: [user_color_scheme, too_big ? 'hide' : ''] } + - last_line = 0 - diff_file.diff_lines.each_with_index do |line, index| - type = line.type @@ -21,7 +22,8 @@ = link_to_new_diff_note(line_code) %td.new_line{data: {linenumber: line.new_pos}} = link_to raw(type == "old" ? " " : line.new_pos) , "##{line_code}", id: line_code - %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw diff_line_content(line.text) + %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}< + = highlight_line(diff_file.new_path, unescape_html(diff_line_content(line.text))) - if @reply_allowed - comments = @line_notes.select { |n| n.line_code == line_code && n.active? }.sort_by(&:created_at) diff --git a/lib/rouge/lexers/gitlab_diff.rb b/lib/rouge/lexers/gitlab_diff.rb new file mode 100644 index 00000000000..e136d47df00 --- /dev/null +++ b/lib/rouge/lexers/gitlab_diff.rb @@ -0,0 +1,20 @@ +Rouge::Token::Tokens.token(:InlineDiff, 'idiff') + +module Rouge + module Lexers + class GitlabDiff < RegexLexer + title "GitLab Diff" + tag 'gitlab_diff' + + state :root do + rule %r{(.*?)} do |match| + token InlineDiff, match[1] + end + + rule /(?:(?!puts 'Hello' world) + end + + it 'should respect the inline diff markup' do + result = highlight_line('demo.rb', "puts 'Hello' world") + expect(result).to eq(expected) + end + end end -- cgit v1.2.1 From c031b9d9cd1ea41ab68f46eb5e630efaf901933a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 23 Dec 2015 19:10:13 -0500 Subject: Set initial state on parent Lexer. #3945 --- lib/rouge/lexers/gitlab_diff.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/rouge/lexers/gitlab_diff.rb b/lib/rouge/lexers/gitlab_diff.rb index e136d47df00..c7aaeb92608 100644 --- a/lib/rouge/lexers/gitlab_diff.rb +++ b/lib/rouge/lexers/gitlab_diff.rb @@ -15,6 +15,10 @@ module Rouge delegate option(:parent_lexer) end end + + start do + option(:parent_lexer).reset! + end end end end -- cgit v1.2.1 From bb96d631537d3d8181f0d3b762603a012219c3e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 30 Dec 2015 00:52:50 -0500 Subject: New implementation for highlighting diff files. #3945 * It is more performant given now we process all the diff file instead of processing line by line. * Multiline comments are highlighted correctly. --- app/helpers/application_helper.rb | 6 --- app/helpers/blob_helper.rb | 15 ------- app/helpers/diff_helper.rb | 6 +-- app/views/projects/diffs/_parallel_view.html.haml | 6 +-- app/views/projects/diffs/_text_file.html.haml | 5 +-- lib/gitlab/diff/file.rb | 4 ++ lib/gitlab/diff/highlight.rb | 55 +++++++++++++++++++++++ lib/gitlab/diff/line.rb | 1 + lib/rouge/lexers/gitlab_diff.rb | 2 +- spec/helpers/blob_helper_spec.rb | 11 ----- 10 files changed, 68 insertions(+), 43 deletions(-) create mode 100644 lib/gitlab/diff/highlight.rb diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index bc4b6ec0327..0b00b9a0702 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -326,10 +326,4 @@ module ApplicationHelper def truncate_first_line(message, length = 50) truncate(message.each_line.first.chomp, length: length) if message end - - def unescape_html(content) - text = CGI.unescapeHTML(content) - text.gsub!(' ', ' ') - text - end end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index bf18673972c..1230002e69c 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -24,21 +24,6 @@ module BlobHelper result end - def highlight_line(blob_name, content, continue: false) - if @previous_blob_name != blob_name - @parent = Rouge::Lexer.guess(filename: blob_name, source: content).new rescue Rouge::Lexers::PlainText.new - @lexer = Rouge::Lexers::GitlabDiff.new(parent_lexer: @parent) - @options = Rouge::Lexers::PlainText === @parent ? {} : { continue: continue } - end - - @previous_blob_name = blob_name - @formatter ||= rouge_formatter(nowrap: true) - - content.sub!(/\A((?:\+|-)\s*)/, '') # Don't format '+' or '-' indicators. - - "#{$1}#{@formatter.format(@lexer.lex(content, @options))}".html_safe - end - def no_highlight_files %w(credits changelog news copying copyright license authors) end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 24134310fc5..2ff8c65e5ca 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -54,9 +54,9 @@ module DiffHelper # right_line_type, right_line_number, right_line_content, right_line_code # ] # - diff_file.diff_lines.each do |line| + diff_file.highlighted_diff_lines.each do |line| - full_line = line.text + full_line = line.highlighted_text type = line.type line_code = generate_line_code(diff_file.file_path, line) line_new = line.new_pos @@ -67,7 +67,7 @@ module DiffHelper if next_line next_line_code = generate_line_code(diff_file.file_path, next_line) next_type = next_line.type - next_line = next_line.text + next_line = next_line.highlighted_text end if type == 'match' || type.nil? diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index c6a9d71e789..4ded4d2daad 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -20,8 +20,7 @@ = link_to raw(line_number_left), "##{line_code_left}", id: line_code_left - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code_left, 'old') - %td.line_content{class: "parallel noteable_line #{type_left} #{line_code_left}", "line_code" => line_code_left }< - = highlight_line(diff_file.new_path, unescape_html(line_content_left)) + %td.line_content{class: "parallel noteable_line #{type_left} #{line_code_left}", "line_code" => line_code_left }= raw(line_content_left) - if type_right == 'new' - new_line_class = 'new' @@ -34,8 +33,7 @@ = link_to raw(line_number_right), "##{new_line_code}", id: new_line_code - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code_right, 'new') - %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", "line_code" => new_line_code}< - = highlight_line(diff_file.new_path, unescape_html(line_content_right)) + %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", "line_code" => new_line_code}= raw(line_content_right) - if @reply_allowed - comments_left, comments_right = organize_comments(type_left, type_right, line_code_left, line_code_right) diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index 78c66a6291e..641e9e5501a 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -6,7 +6,7 @@ %table.text-file.code.js-syntax-highlight{ class: [user_color_scheme, too_big ? 'hide' : ''] } - last_line = 0 - - diff_file.diff_lines.each_with_index do |line, index| + - diff_file.highlighted_diff_lines.each_with_index do |line, index| - type = line.type - last_line = line.new_pos - line_code = generate_line_code(diff_file.file_path, line) @@ -22,8 +22,7 @@ = link_to_new_diff_note(line_code) %td.new_line{data: {linenumber: line.new_pos}} = link_to raw(type == "old" ? " " : line.new_pos) , "##{line_code}", id: line_code - %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}< - = highlight_line(diff_file.new_path, unescape_html(diff_line_content(line.text))) + %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw(diff_line_content(line.highlighted_text)) - if @reply_allowed - comments = @line_notes.select { |n| n.line_code == line_code && n.active? }.sort_by(&:created_at) diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index 79061cd0141..ff8765b8e26 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -15,6 +15,10 @@ module Gitlab @lines ||= parser.parse(raw_diff.lines) end + def highlighted_diff_lines + Gitlab::Diff::Highlight.process_diff_lines(self) + end + def mode_changed? !!(diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode) end diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb new file mode 100644 index 00000000000..f10b55eb00b --- /dev/null +++ b/lib/gitlab/diff/highlight.rb @@ -0,0 +1,55 @@ +module Gitlab + module Diff + class Highlight + def self.process_diff_lines(diff_file) + processor = new(diff_file) + processor.highlight + end + + def initialize(diff_file) + text_lines = diff_file.diff_lines.map(&:text) + @diff_file = diff_file + @diff_lines = diff_file.diff_lines + @diff_line_prefixes = text_lines.map { |line| line.sub!(/\A((\+|\-)\s*)/, '');$1 } + @raw_lines = text_lines.join("\n") + end + + def highlight + @code = unescape_html(@raw_lines) + @highlighted_code = formatter.format(lexer.lex(@code)) + + update_diff_lines + end + + private + + def update_diff_lines + @highlighted_code.lines.each_with_index do |line, i| + @diff_lines[i].highlighted_text = "#{@diff_line_prefixes[i]}#{line}" + end + + @diff_lines + end + + def lexer + parent = Rouge::Lexer.guess(filename: @diff_file.new_path, source: @code).new rescue Rouge::Lexers::PlainText.new + Rouge::Lexers::GitlabDiff.new(parent_lexer: parent) + end + + def unescape_html(content) + text = CGI.unescapeHTML(content) + text.gsub!(' ', ' ') + text + end + + def formatter + @formatter ||= Rouge::Formatters::HTMLGitlab.new( + nowrap: true, + cssclass: 'code highlight', + lineanchors: true, + lineanchorsid: 'LC' + ) + end + end + end +end diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb index 0072194606e..c48c69fb344 100644 --- a/lib/gitlab/diff/line.rb +++ b/lib/gitlab/diff/line.rb @@ -2,6 +2,7 @@ module Gitlab module Diff class Line attr_reader :type, :text, :index, :old_pos, :new_pos + attr_accessor :highlighted_text def initialize(text, type, index, old_pos, new_pos) @text, @type, @index = text, type, index diff --git a/lib/rouge/lexers/gitlab_diff.rb b/lib/rouge/lexers/gitlab_diff.rb index c7aaeb92608..d91dd6c4245 100644 --- a/lib/rouge/lexers/gitlab_diff.rb +++ b/lib/rouge/lexers/gitlab_diff.rb @@ -11,7 +11,7 @@ module Rouge token InlineDiff, match[1] end - rule /(?:(?!puts 'Hello' world) - end - - it 'should respect the inline diff markup' do - result = highlight_line('demo.rb', "puts 'Hello' world") - expect(result).to eq(expected) - end - end end -- cgit v1.2.1 From b74f36c9caae38a1d62c18281d8240ec5905c5d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 30 Dec 2015 13:10:28 -0500 Subject: Fix Rubocop complain. #3945 --- lib/gitlab/diff/highlight.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index f10b55eb00b..adb437abed2 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -44,11 +44,11 @@ module Gitlab def formatter @formatter ||= Rouge::Formatters::HTMLGitlab.new( - nowrap: true, - cssclass: 'code highlight', - lineanchors: true, - lineanchorsid: 'LC' - ) + nowrap: true, + cssclass: 'code highlight', + lineanchors: true, + lineanchorsid: 'LC' + ) end end end -- cgit v1.2.1 From 7de90f4b53f865dc417d022a9133372e57274549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 30 Dec 2015 18:42:11 -0500 Subject: Fix broken spec and small refactor. #3945 --- app/helpers/diff_helper.rb | 4 +- app/views/projects/diffs/_text_file.html.haml | 2 +- lib/gitlab/diff/highlight.rb | 2 +- lib/gitlab/diff/line.rb | 4 +- spec/fixtures/parallel_diff_result.yml | 274 ++++++++++++++++++++++++++ spec/helpers/diff_helper_spec.rb | 30 +-- 6 files changed, 281 insertions(+), 35 deletions(-) create mode 100644 spec/fixtures/parallel_diff_result.yml diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 2ff8c65e5ca..22deb69cec5 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -56,7 +56,7 @@ module DiffHelper # diff_file.highlighted_diff_lines.each do |line| - full_line = line.highlighted_text + full_line = line.text type = line.type line_code = generate_line_code(diff_file.file_path, line) line_new = line.new_pos @@ -67,7 +67,7 @@ module DiffHelper if next_line next_line_code = generate_line_code(diff_file.file_path, next_line) next_type = next_line.type - next_line = next_line.highlighted_text + next_line = next_line.text end if type == 'match' || type.nil? diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index 641e9e5501a..059d0baf45e 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -22,7 +22,7 @@ = link_to_new_diff_note(line_code) %td.new_line{data: {linenumber: line.new_pos}} = link_to raw(type == "old" ? " " : line.new_pos) , "##{line_code}", id: line_code - %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw(diff_line_content(line.highlighted_text)) + %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw(diff_line_content(line.text)) - if @reply_allowed - comments = @line_notes.select { |n| n.line_code == line_code && n.active? }.sort_by(&:created_at) diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index adb437abed2..d0c2e3670c6 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -25,7 +25,7 @@ module Gitlab def update_diff_lines @highlighted_code.lines.each_with_index do |line, i| - @diff_lines[i].highlighted_text = "#{@diff_line_prefixes[i]}#{line}" + @diff_lines[i].text = "#{@diff_line_prefixes[i]}#{line}" end @diff_lines diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb index c48c69fb344..03730b435ad 100644 --- a/lib/gitlab/diff/line.rb +++ b/lib/gitlab/diff/line.rb @@ -1,8 +1,8 @@ module Gitlab module Diff class Line - attr_reader :type, :text, :index, :old_pos, :new_pos - attr_accessor :highlighted_text + attr_reader :type, :index, :old_pos, :new_pos + attr_accessor :text def initialize(text, type, index, old_pos, new_pos) @text, @type, @index = text, type, index diff --git a/spec/fixtures/parallel_diff_result.yml b/spec/fixtures/parallel_diff_result.yml new file mode 100644 index 00000000000..abbb364f61f --- /dev/null +++ b/spec/fixtures/parallel_diff_result.yml @@ -0,0 +1,274 @@ +--- +- - match + - 6 + - | + @@ -6,12 +6,18 @@ module Popen + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 + - match + - 6 + - | + @@ -6,12 +6,18 @@ module Popen + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 +- - + - 6 + - | + + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 + - + - 6 + - | + + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 +- - + - 7 + - | + def popen(cmd, path=nil) + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7 + - + - 7 + - | + def popen(cmd, path=nil) + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7 +- - + - 8 + - | + unless cmd.is_a?(Array) + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8 + - + - 8 + - | + unless cmd.is_a?(Array) + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8 +- - old + - 9 + - | + - raise "System commands must be given as an array of strings" + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9 + - new + - 9 + - | + + raise RuntimeError, "System commands must be given as an array of strings" + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9 +- - + - 10 + - | + end + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10 + - + - 10 + - | + end + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10 +- - + - 11 + - | + + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11 + - + - 11 + - | + + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11 +- - + - 12 + - | + path ||= Dir.pwd + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12 + - + - 12 + - | + path ||= Dir.pwd + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12 +- - old + - 13 + - | + - vars = { "PWD" => path } + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_13_13 + - old + - + - " " + - +- - old + - 14 + - | + - options = { chdir: path } + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_13 + - new + - 13 + - | + + + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_13 +- - + - + - " " + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14 + - new + - 14 + - | + + vars = { + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14 +- - + - + - " " + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15 + - new + - 15 + - | + + "PWD" => path + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15 +- - + - + - " " + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16 + - new + - 16 + - | + + } + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16 +- - + - + - " " + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17 + - new + - 17 + - | + + + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17 +- - + - + - " " + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18 + - new + - 18 + - | + + options = { + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18 +- - + - + - " " + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19 + - new + - 19 + - | + + chdir: path + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19 +- - + - + - " " + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20 + - new + - 20 + - | + + } + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20 +- - + - 15 + - | + + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21 + - + - 21 + - | + + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21 +- - + - 16 + - | + unless File.directory?(path) + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22 + - + - 22 + - | + unless File.directory?(path) + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22 +- - + - 17 + - | + FileUtils.mkdir_p(path) + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23 + - + - 23 + - | + FileUtils.mkdir_p(path) + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23 +- - match + - 19 + - | + @@ -19,6 +25,7 @@ module Popen + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 + - match + - 25 + - | + @@ -19,6 +25,7 @@ module Popen + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 +- - + - 19 + - | + + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 + - + - 25 + - | + + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 +- - + - 20 + - | + @cmd_output = "" + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26 + - + - 26 + - | + @cmd_output = "" + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26 +- - + - 21 + - | + @cmd_status = 0 + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27 + - + - 27 + - | + @cmd_status = 0 + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27 +- - + - + - " " + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28 + - new + - 28 + - | + + + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28 +- - + - 22 + - | + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29 + - + - 29 + - | + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29 +- - + - 23 + - | + @cmd_output << stdout.read + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30 + - + - 30 + - | + @cmd_output << stdout.read + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30 +- - + - 24 + - @cmd_output << stderr.read + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31 + - + - 31 + - @cmd_output << stderr.read + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31 diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 7c96a74e581..0501c2e8c29 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -131,34 +131,6 @@ describe DiffHelper do end def parallel_diff_result_array - [ - ["match", 6, "@@ -6,12 +6,18 @@ module Popen", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6", "match", 6, "@@ -6,12 +6,18 @@ module Popen", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6"], - [nil, 6, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6", nil, 6, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6"], [nil, 7, " def popen(cmd, path=nil)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7", nil, 7, " def popen(cmd, path=nil)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"], - [nil, 8, " unless cmd.is_a?(Array)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8", nil, 8, " unless cmd.is_a?(Array)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"], - ["old", 9, "- raise "System commands must be given as an array of strings"", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9", "new", 9, "+ raise RuntimeError, "System commands must be given as an array of strings"", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], - [nil, 10, " end", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10", nil, 10, " end", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"], - [nil, 11, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11", nil, 11, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11"], - [nil, 12, " path ||= Dir.pwd", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12", nil, 12, " path ||= Dir.pwd", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12"], - ["old", 13, "- vars = { "PWD" => path }", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_13_13", "old", nil, " ", nil], - ["old", 14, "- options = { chdir: path }", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_13", "new", 13, "+", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_13"], - [nil, nil, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14", "new", 14, "+ vars = {", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14"], - [nil, nil, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15", "new", 15, "+ "PWD" => path", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"], - [nil, nil, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16", "new", 16, "+ }", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16"], - [nil, nil, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17", "new", 17, "+", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17"], - [nil, nil, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18", "new", 18, "+ options = {", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18"], - [nil, nil, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19", "new", 19, "+ chdir: path", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19"], - [nil, nil, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20", "new", 20, "+ }", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20"], - [nil, 15, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21", nil, 21, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21"], - [nil, 16, " unless File.directory?(path)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22", nil, 22, " unless File.directory?(path)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22"], - [nil, 17, " FileUtils.mkdir_p(path)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23", nil, 23, " FileUtils.mkdir_p(path)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23"], - ["match", 19, "@@ -19,6 +25,7 @@ module Popen", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25", "match", 25, "@@ -19,6 +25,7 @@ module Popen", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25"], - [nil, 19, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25", nil, 25, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25"], - [nil, 20, " @cmd_output = """, "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26", nil, 26, " @cmd_output = """, "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26"], - [nil, 21, " @cmd_status = 0", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27", nil, 27, " @cmd_status = 0", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27"], - [nil, nil, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28", "new", 28, "+", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28"], - [nil, 22, " Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29", nil, 29, " Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29"], - [nil, 23, " @cmd_output << stdout.read", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30", nil, 30, " @cmd_output << stdout.read", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30"], - [nil, 24, " @cmd_output << stderr.read", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31", nil, 31, " @cmd_output << stderr.read", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31"] - ] + YAML.load_file("#{Rails.root}/spec/fixtures/parallel_diff_result.yml") end end -- cgit v1.2.1 From d83275620a0eeaf17a2958e59e2b4d6f217c6464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 30 Dec 2015 20:18:40 -0500 Subject: Add specs for Gitlab::Diff::Highlight. #3945 --- spec/lib/gitlab/diff/highlight_spec.rb | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 spec/lib/gitlab/diff/highlight_spec.rb diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb new file mode 100644 index 00000000000..2a827a08dba --- /dev/null +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Gitlab::Diff::Highlight, lib: true do + include RepoHelpers + + let(:project) { create(:project) } + let(:commit) { project.commit(sample_commit.id) } + let(:diff) { commit.diffs.first } + let(:diff_file) { Gitlab::Diff::File.new(diff) } + + describe '.process_diff_lines' do + let(:diff_lines) { Gitlab::Diff::Highlight.process_diff_lines(diff_file) } + + it 'should return Gitlab::Diff::Line elements' do + expect(diff_lines.first).to be_an_instance_of(Gitlab::Diff::Line) + end + + it 'should highlight the code' do + code = %Q{ def popen(cmd, path=nil)\n} + + expect(diff_lines[2].text).to eq(code) + end + + it 'should keep the inline diff markup' do + expect(diff_lines[5].text).to match(Regexp.new(Regexp.escape('RuntimeError, '))) + end + end +end -- cgit v1.2.1 From 8b079315d98a8ccf852592148632c6f052d9cb55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 30 Dec 2015 21:23:50 -0500 Subject: A bit of refactoring. #3945 --- lib/gitlab/diff/file.rb | 2 +- lib/gitlab/diff/highlight.rb | 16 ++++++++-------- lib/rouge/lexers/gitlab_diff.rb | 2 ++ spec/lib/gitlab/diff/highlight_spec.rb | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index ff8765b8e26..69b38a32eeb 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -16,7 +16,7 @@ module Gitlab end def highlighted_diff_lines - Gitlab::Diff::Highlight.process_diff_lines(self) + Gitlab::Diff::Highlight.process_diff_lines(new_path, diff_lines) end def mode_changed? diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index d0c2e3670c6..40a54ede2bb 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -1,15 +1,15 @@ module Gitlab module Diff class Highlight - def self.process_diff_lines(diff_file) - processor = new(diff_file) + def self.process_diff_lines(file_name, diff_lines) + processor = new(file_name, diff_lines) processor.highlight end - def initialize(diff_file) - text_lines = diff_file.diff_lines.map(&:text) - @diff_file = diff_file - @diff_lines = diff_file.diff_lines + def initialize(file_name, diff_lines) + text_lines = diff_lines.map(&:text) + @file_name = file_name + @diff_lines = diff_lines @diff_line_prefixes = text_lines.map { |line| line.sub!(/\A((\+|\-)\s*)/, '');$1 } @raw_lines = text_lines.join("\n") end @@ -32,7 +32,7 @@ module Gitlab end def lexer - parent = Rouge::Lexer.guess(filename: @diff_file.new_path, source: @code).new rescue Rouge::Lexers::PlainText.new + parent = Rouge::Lexer.guess(filename: @file_name, source: @code).new rescue Rouge::Lexers::PlainText.new Rouge::Lexers::GitlabDiff.new(parent_lexer: parent) end @@ -43,7 +43,7 @@ module Gitlab end def formatter - @formatter ||= Rouge::Formatters::HTMLGitlab.new( + Rouge::Formatters::HTMLGitlab.new( nowrap: true, cssclass: 'code highlight', lineanchors: true, diff --git a/lib/rouge/lexers/gitlab_diff.rb b/lib/rouge/lexers/gitlab_diff.rb index d91dd6c4245..cbf272ee1de 100644 --- a/lib/rouge/lexers/gitlab_diff.rb +++ b/lib/rouge/lexers/gitlab_diff.rb @@ -2,6 +2,8 @@ Rouge::Token::Tokens.token(:InlineDiff, 'idiff') module Rouge module Lexers + # This new Lexer is required in order to avoid the inline diff markup + # to be tokenized, it will be rendered as raw HTML code if that happens. class GitlabDiff < RegexLexer title "GitLab Diff" tag 'gitlab_diff' diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index 2a827a08dba..80083c15cff 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::Diff::Highlight, lib: true do let(:diff_file) { Gitlab::Diff::File.new(diff) } describe '.process_diff_lines' do - let(:diff_lines) { Gitlab::Diff::Highlight.process_diff_lines(diff_file) } + let(:diff_lines) { Gitlab::Diff::Highlight.process_diff_lines(diff_file.new_path, diff_file.diff_lines) } it 'should return Gitlab::Diff::Line elements' do expect(diff_lines.first).to be_an_instance_of(Gitlab::Diff::Line) -- cgit v1.2.1 From fd100e1ef1726418c81ab8833cf8bcf86fab6eef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 30 Dec 2015 21:44:12 -0500 Subject: Don't modify "match" diff lines. #3945 --- lib/gitlab/diff/highlight.rb | 7 ++++++- spec/lib/gitlab/diff/highlight_spec.rb | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index 40a54ede2bb..c780ea21775 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -25,7 +25,12 @@ module Gitlab def update_diff_lines @highlighted_code.lines.each_with_index do |line, i| - @diff_lines[i].text = "#{@diff_line_prefixes[i]}#{line}" + diff_line = @diff_lines[i] + + # ignore highlighting for "match" lines + next if diff_line.type == 'match' + + diff_line.text = "#{@diff_line_prefixes[i]}#{line}" end @diff_lines diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index 80083c15cff..54621f773d7 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -24,5 +24,10 @@ describe Gitlab::Diff::Highlight, lib: true do it 'should keep the inline diff markup' do expect(diff_lines[5].text).to match(Regexp.new(Regexp.escape('RuntimeError, '))) end + + it 'should not modify "match" lines' do + expect(diff_lines[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') + expect(diff_lines[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') + end end end -- cgit v1.2.1 From 3fbcf52ec8decc3a4e331d52b2f47d7b85d399cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Thu, 31 Dec 2015 01:05:52 -0500 Subject: Apply syntax highlighting when expanding diff plus some refactor. #3945 --- app/controllers/projects/blob_controller.rb | 2 +- app/views/projects/blob/diff.html.haml | 2 +- lib/gitlab/diff/highlight.rb | 55 ++++++++++++++++++++++------- spec/lib/gitlab/diff/highlight_spec.rb | 42 +++++++++++++++------- 4 files changed, 74 insertions(+), 27 deletions(-) diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index c56a3497bb2..d22c7b550b0 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -66,7 +66,7 @@ class Projects::BlobController < Projects::ApplicationController def diff @form = UnfoldForm.new(params) - @lines = @blob.data.lines[@form.since - 1..@form.to - 1] + @lines = Gitlab::Diff::Highlight.process_diff_lines(@blob.name, @blob.data.lines[@form.since - 1..@form.to - 1]) if @form.bottom? @match_line = '' diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index f3b01ff3288..9d8f6ecb3ac 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -11,7 +11,7 @@ %td.old_line.diff-line-num{data: {linenumber: line_old}} = link_to raw(line_old), "#" %td.new_line= link_to raw(line_new) , "#" - %td.line_content.noteable_line= ' ' * @form.indent + line + %td.line_content.noteable_line= raw("#{' ' * @form.indent}#{line}") - if @form.unfold? && @form.bottom? && @form.to < @blob.loc %tr.line_holder{ id: @form.to } diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index c780ea21775..7f340de65cc 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -1,31 +1,62 @@ module Gitlab module Diff class Highlight - def self.process_diff_lines(file_name, diff_lines) - processor = new(file_name, diff_lines) + # Apply syntax highlight to provided source code + # + # file_name - The file name related to the code. + # lines - It can be an Array of Gitlab::Diff::Line objects or simple Strings. + # When passing Strings you need to provide the required 'end of lines' + # chars ("\n") for each String given that we don't append them automatically. + # + # Returns an Array with the processed items. + def self.process_diff_lines(file_name, lines) + processor = new(file_name, lines) processor.highlight end - def initialize(file_name, diff_lines) - text_lines = diff_lines.map(&:text) - @file_name = file_name - @diff_lines = diff_lines - @diff_line_prefixes = text_lines.map { |line| line.sub!(/\A((\+|\-)\s*)/, '');$1 } - @raw_lines = text_lines.join("\n") + def initialize(file_name, lines) + @file_name = file_name + @lines = lines end def highlight - @code = unescape_html(@raw_lines) + return [] if @lines.empty? + + extract_line_prefixes + + @code = unescape_html(raw_content) @highlighted_code = formatter.format(lexer.lex(@code)) - update_diff_lines + is_diff_line? ? update_diff_lines : @highlighted_code.lines end private + def is_diff_line? + @lines.first.is_a?(Gitlab::Diff::Line) + end + + def text_lines + @text_lines ||= (is_diff_line? ? @lines.map(&:text) : @lines) + end + + def raw_content + @raw_content ||= text_lines.join(is_diff_line? ? "\n" : nil) + end + + def extract_line_prefixes + @diff_line_prefixes ||= begin + if is_diff_line? + text_lines.map { |line| line.sub!(/\A((\+|\-)\s*)/, '');$1 } + else + [] + end + end + end + def update_diff_lines @highlighted_code.lines.each_with_index do |line, i| - diff_line = @diff_lines[i] + diff_line = @lines[i] # ignore highlighting for "match" lines next if diff_line.type == 'match' @@ -33,7 +64,7 @@ module Gitlab diff_line.text = "#{@diff_line_prefixes[i]}#{line}" end - @diff_lines + @lines end def lexer diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index 54621f773d7..fc5cb894d2a 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -9,25 +9,41 @@ describe Gitlab::Diff::Highlight, lib: true do let(:diff_file) { Gitlab::Diff::File.new(diff) } describe '.process_diff_lines' do - let(:diff_lines) { Gitlab::Diff::Highlight.process_diff_lines(diff_file.new_path, diff_file.diff_lines) } + context 'when processing Gitlab::Diff::Line objects' do + let(:diff_lines) { Gitlab::Diff::Highlight.process_diff_lines(diff_file.new_path, diff_file.diff_lines) } - it 'should return Gitlab::Diff::Line elements' do - expect(diff_lines.first).to be_an_instance_of(Gitlab::Diff::Line) - end + it 'should return Gitlab::Diff::Line elements' do + expect(diff_lines.first).to be_an_instance_of(Gitlab::Diff::Line) + end - it 'should highlight the code' do - code = %Q{ def popen(cmd, path=nil)\n} + it 'should highlight the code' do + code = %Q{ def popen(cmd, path=nil)\n} - expect(diff_lines[2].text).to eq(code) - end + expect(diff_lines[2].text).to eq(code) + end + + it 'should keep the inline diff markup' do + expect(diff_lines[5].text).to match(Regexp.new(Regexp.escape('RuntimeError, '))) + end - it 'should keep the inline diff markup' do - expect(diff_lines[5].text).to match(Regexp.new(Regexp.escape('RuntimeError, '))) + it 'should not modify "match" lines' do + expect(diff_lines[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') + expect(diff_lines[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') + end end - it 'should not modify "match" lines' do - expect(diff_lines[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') - expect(diff_lines[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') + context 'when processing raw lines' do + let(:lines) { ["puts 'Hello'\n", "# A comment"] } + let(:highlighted_lines) { Gitlab::Diff::Highlight.process_diff_lines('demo.rb', lines) } + + it 'should highlight the code' do + line_1 = %Q{puts 'Hello'\n} + line_2 = %Q{# A comment} + + expect(highlighted_lines[0]).to eq(line_1) + expect(highlighted_lines[1]).to eq(line_2) + end end + end end -- cgit v1.2.1 From 795ecb498c74d7fbc2db461b3d37ff10b350a3c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Thu, 31 Dec 2015 01:46:52 -0500 Subject: Fix broken spec. #3945 --- spec/fixtures/parallel_diff_result.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/spec/fixtures/parallel_diff_result.yml b/spec/fixtures/parallel_diff_result.yml index abbb364f61f..16c4bac96df 100644 --- a/spec/fixtures/parallel_diff_result.yml +++ b/spec/fixtures/parallel_diff_result.yml @@ -1,13 +1,11 @@ --- - - match - 6 - - | - @@ -6,12 +6,18 @@ module Popen + - '@@ -6,12 +6,18 @@ module Popen' - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 - match - 6 - - | - @@ -6,12 +6,18 @@ module Popen + - '@@ -6,12 +6,18 @@ module Popen' - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 - - - 6 @@ -193,13 +191,11 @@ - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23 - - match - 19 - - | - @@ -19,6 +25,7 @@ module Popen + - '@@ -19,6 +25,7 @@ module Popen' - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 - match - 25 - - | - @@ -19,6 +25,7 @@ module Popen + - '@@ -19,6 +25,7 @@ module Popen' - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 - - - 19 -- cgit v1.2.1 From 776d70d11b822e5991d518e91b138853275ef42c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 6 Jan 2016 19:54:36 -0500 Subject: Use #html_safe instead of #raw in some diff views. #3945 --- app/views/projects/blob/diff.html.haml | 2 +- app/views/projects/diffs/_parallel_view.html.haml | 4 ++-- app/views/projects/diffs/_text_file.html.haml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index 9d8f6ecb3ac..ef23876677a 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -11,7 +11,7 @@ %td.old_line.diff-line-num{data: {linenumber: line_old}} = link_to raw(line_old), "#" %td.new_line= link_to raw(line_new) , "#" - %td.line_content.noteable_line= raw("#{' ' * @form.indent}#{line}") + %td.line_content.noteable_line= "#{' ' * @form.indent}#{line}".html_safe - if @form.unfold? && @form.bottom? && @form.to < @blob.loc %tr.line_holder{ id: @form.to } diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 4ded4d2daad..6a7d4f2db20 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -20,7 +20,7 @@ = link_to raw(line_number_left), "##{line_code_left}", id: line_code_left - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code_left, 'old') - %td.line_content{class: "parallel noteable_line #{type_left} #{line_code_left}", "line_code" => line_code_left }= raw(line_content_left) + %td.line_content{class: "parallel noteable_line #{type_left} #{line_code_left}", "line_code" => line_code_left }= line_content_left.html_safe - if type_right == 'new' - new_line_class = 'new' @@ -33,7 +33,7 @@ = link_to raw(line_number_right), "##{new_line_code}", id: new_line_code - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code_right, 'new') - %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", "line_code" => new_line_code}= raw(line_content_right) + %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", "line_code" => new_line_code}= line_content_right.html_safe - if @reply_allowed - comments_left, comments_right = organize_comments(type_left, type_right, line_code_left, line_code_right) diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index 059d0baf45e..5509d5403b0 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -21,8 +21,8 @@ - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code) %td.new_line{data: {linenumber: line.new_pos}} - = link_to raw(type == "old" ? " " : line.new_pos) , "##{line_code}", id: line_code - %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw(diff_line_content(line.text)) + = link_to raw(type == "old" ? " " : line.new_pos), "##{line_code}", id: line_code + %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= diff_line_content(line.text).html_safe - if @reply_allowed - comments = @line_notes.select { |n| n.line_code == line_code && n.active? }.sort_by(&:created_at) -- cgit v1.2.1 From 1494bb3f253b2e37065080c27af22ec531966ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 6 Jan 2016 21:01:44 -0500 Subject: Force white theme when viewing diffs. #3945 --- app/views/projects/diffs/_parallel_view.html.haml | 2 +- app/views/projects/diffs/_text_file.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 6a7d4f2db20..2a43bbd11f2 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -1,5 +1,5 @@ / Side-by-side diff view -%div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight{ class: user_color_scheme } +%div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight.white %table - parallel_diff(diff_file, index).each do |line| - type_left = line[0] diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index 5509d5403b0..8e86219155f 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -3,7 +3,7 @@ .suppressed-container %a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show. -%table.text-file.code.js-syntax-highlight{ class: [user_color_scheme, too_big ? 'hide' : ''] } +%table.text-file.code.js-syntax-highlight.white{ class: too_big ? 'hide' : '' } - last_line = 0 - diff_file.highlighted_diff_lines.each_with_index do |line, index| -- cgit v1.2.1 From 21958a393955037318141d39fbe14b1c2e842cab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Thu, 7 Jan 2016 13:45:19 -0500 Subject: Add some styling for syntax highlighting themes. #3945 --- app/assets/stylesheets/highlight/dark.scss | 14 ++++++++++++++ app/assets/stylesheets/highlight/monokai.scss | 18 ++++++++++++++++++ app/assets/stylesheets/highlight/solarized_dark.scss | 14 ++++++++++++++ app/assets/stylesheets/highlight/solarized_light.scss | 15 +++++++++++++++ app/assets/stylesheets/pages/diff.scss | 15 +++++++++++++++ app/views/projects/diffs/_parallel_view.html.haml | 2 +- app/views/projects/diffs/_text_file.html.haml | 2 +- 7 files changed, 78 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index 6a2b25ddc67..f7d1334705f 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -90,4 +90,18 @@ .vg { color: #cc6666 } /* Name.Variable.Global */ .vi { color: #cc6666 } /* Name.Variable.Instance */ .il { color: #de935f } /* Literal.Number.Integer.Long */ + + .line_holder { + &.new .old_line, + &.new .new_line, + &.new .line_content { + @include diff_background(255, 255, 255, #808080); + } + + &.old .old_line, + &.old .new_line, + &.old .line_content { + @include diff_background(255, 51, 51, #808080); + } + } } diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index 8560c3c490f..cc03ed6ae45 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -90,4 +90,22 @@ .gu { color: #75715e; } /* Generic.Subheading & Diff Unified/Comment? */ .gd { color: #f92672; } /* Generic.Deleted & Diff Deleted */ .gi { color: #a6e22e; } /* Generic.Inserted & Diff Inserted */ + + .line_holder { + &.parallel .new.new_line, + &.parallel .new.line_content, + &.new .old_line, + &.new .new_line, + &.new .line_content { + @include diff_background(156, 175, 183, #808080); + } + + &.parallel .old.old_line, + &.parallel .old.line_content, + &.old .old_line, + &.old .new_line, + &.old .line_content { + @include diff_background(254, 147, 140, #808080); + } + } } diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index 7d489a9666b..2c3648274cf 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -111,4 +111,18 @@ .vg { color: #268bd2 } /* Name.Variable.Global */ .vi { color: #268bd2 } /* Name.Variable.Instance */ .il { color: #2aa198 } /* Literal.Number.Integer.Long */ + + .line_holder { + &.new .old_line, + &.new .new_line, + &.new .line_content { + @include diff_background(255, 255, 255, #808080); + } + + &.old .old_line, + &.old .new_line, + &.old .line_content { + @include diff_background(255, 51, 51, #808080); + } + } } diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index 200ed346446..c16473ffe66 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -111,4 +111,19 @@ .vg { color: #268bd2 } /* Name.Variable.Global */ .vi { color: #268bd2 } /* Name.Variable.Instance */ .il { color: #2aa198 } /* Literal.Number.Integer.Long */ + + + .line_holder { + &.new .old_line, + &.new .new_line, + &.new .line_content { + @include diff_background(92, 164, 169, #FAF3DD); + } + + &.old .old_line, + &.old .new_line, + &.old .line_content { + @include diff_background(237, 106, 90, #FAF3DD); + } + } } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index afd6fb73675..caaad1e31d3 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -402,3 +402,18 @@ right: 15px; } } + +@mixin diff_background($r, $g, $b, $custom-border) { + /* Fallback for web browsers that doesn't support RGBa */ + background: rgb($r, $g, $b); + /* RGBa with 0.3 opacity */ + background: rgba($r, $g, $b, 0.3); + + &.new_line, &.old_line { + border-right-color: $custom-border !important; + } + + &.line_content span.idiff { + background: rgb($r, $g, $b); + } +} diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 2a43bbd11f2..1ad54d1848b 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -1,5 +1,5 @@ / Side-by-side diff view -%div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight.white +%div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight %table - parallel_diff(diff_file, index).each do |line| - type_left = line[0] diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index 8e86219155f..f1b8cba5e55 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -3,7 +3,7 @@ .suppressed-container %a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show. -%table.text-file.code.js-syntax-highlight.white{ class: too_big ? 'hide' : '' } +%table.text-file.code.js-syntax-highlight{ class: too_big ? 'hide' : '' } - last_line = 0 - diff_file.highlighted_diff_lines.each_with_index do |line, index| -- cgit v1.2.1 From f1f4fdf778245cab74ff9cda2a421315c21a99aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Thu, 7 Jan 2016 21:08:57 -0500 Subject: Don't process inline diffs on backend. #3945 --- lib/gitlab/diff/parser.rb | 5 +- lib/gitlab/inline_diff.rb | 104 ------------------------------------ spec/lib/gitlab/inline_diff_spec.rb | 39 -------------- 3 files changed, 1 insertion(+), 147 deletions(-) delete mode 100644 lib/gitlab/inline_diff.rb delete mode 100644 spec/lib/gitlab/inline_diff_spec.rb diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb index 7015fe36c3d..177bad4b1cf 100644 --- a/lib/gitlab/diff/parser.rb +++ b/lib/gitlab/diff/parser.rb @@ -11,13 +11,10 @@ module Gitlab line_new = 1 type = nil - lines_arr = ::Gitlab::InlineDiff.processing lines - - lines_arr.each do |line| + @lines.each do |line| next if filename?(line) full_line = html_escape(line.gsub(/\n/, '')) - full_line = ::Gitlab::InlineDiff.replace_markers full_line if line.match(/^@@ -/) type = "match" diff --git a/lib/gitlab/inline_diff.rb b/lib/gitlab/inline_diff.rb deleted file mode 100644 index 44507bde25d..00000000000 --- a/lib/gitlab/inline_diff.rb +++ /dev/null @@ -1,104 +0,0 @@ -module Gitlab - class InlineDiff - class << self - - START = "#!idiff-start!#" - FINISH = "#!idiff-finish!#" - - def processing(diff_arr) - indexes = _indexes_of_changed_lines diff_arr - - indexes.each do |index| - first_line = diff_arr[index+1] - second_line = diff_arr[index+2] - - # Skip inline diff if empty line was replaced with content - next if first_line == "-\n" - - first_token = find_first_token(first_line, second_line) - apply_first_token(diff_arr, index, first_token) - - last_token = find_last_token(first_line, second_line, first_token) - apply_last_token(diff_arr, index, last_token) - end - - diff_arr - end - - def apply_first_token(diff_arr, index, first_token) - start = first_token + START - - if first_token.empty? - # In case if we remove string of spaces in commit - diff_arr[index+1].sub!("-", "-" => "-#{START}") - diff_arr[index+2].sub!("+", "+" => "+#{START}") - else - diff_arr[index+1].sub!(first_token, first_token => start) - diff_arr[index+2].sub!(first_token, first_token => start) - end - end - - def apply_last_token(diff_arr, index, last_token) - # This is tricky: escape backslashes so that `sub` doesn't interpret them - # as backreferences. Regexp.escape does NOT do the right thing. - replace_token = FINISH + last_token.gsub(/\\/, '\&\&') - diff_arr[index+1].sub!(/#{Regexp.escape(last_token)}$/, replace_token) - diff_arr[index+2].sub!(/#{Regexp.escape(last_token)}$/, replace_token) - end - - def find_first_token(first_line, second_line) - max_length = [first_line.size, second_line.size].max - first_the_same_symbols = 0 - - (0..max_length + 1).each do |i| - first_the_same_symbols = i - 1 - - if first_line[i] != second_line[i] && i > 0 - break - end - end - - first_line[0..first_the_same_symbols][1..-1] - end - - def find_last_token(first_line, second_line, first_token) - max_length = [first_line.size, second_line.size].max - last_the_same_symbols = 0 - - (1..max_length + 1).each do |i| - last_the_same_symbols = -i - shortest_line = second_line.size > first_line.size ? first_line : second_line - - if (first_line[-i] != second_line[-i]) || "#{first_token}#{START}".size == shortest_line[1..-i].size - break - end - end - - last_the_same_symbols += 1 - first_line[last_the_same_symbols..-1] - end - - def _indexes_of_changed_lines(diff_arr) - chain_of_first_symbols = "" - diff_arr.each_with_index do |line, i| - chain_of_first_symbols += line[0] - end - chain_of_first_symbols.gsub!(/[^\-\+]/, "#") - - offset = 0 - indexes = [] - while index = chain_of_first_symbols.index("#-+#", offset) - indexes << index - offset = index + 1 - end - indexes - end - - def replace_markers(line) - line.gsub!(START, "") - line.gsub!(FINISH, "") - line - end - end - end -end diff --git a/spec/lib/gitlab/inline_diff_spec.rb b/spec/lib/gitlab/inline_diff_spec.rb deleted file mode 100644 index c690c195112..00000000000 --- a/spec/lib/gitlab/inline_diff_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'spec_helper' - -describe Gitlab::InlineDiff, lib: true do - describe '#processing' do - let(:diff) do - < Date: Thu, 7 Jan 2016 22:37:01 -0500 Subject: Change strategy to highlight diffs. #3945 Now we apply syntax highlighting to the whole old and new files. This basically help us to highlight adequately multiline content. --- app/controllers/application_controller.rb | 1 + app/controllers/projects/blob_controller.rb | 7 ++- app/controllers/projects/commit_controller.rb | 1 + app/controllers/projects/compare_controller.rb | 1 + .../projects/merge_requests_controller.rb | 2 + app/helpers/diff_helper.rb | 4 +- app/views/projects/commit/show.html.haml | 3 +- app/views/projects/compare/show.html.haml | 2 +- app/views/projects/diffs/_diffs.html.haml | 2 +- .../projects/merge_requests/_new_submit.html.haml | 2 +- .../projects/merge_requests/show/_diffs.html.haml | 2 +- lib/gitlab/diff/file.rb | 8 ++- lib/gitlab/diff/highlight.rb | 70 ++++++++++++++++------ 13 files changed, 76 insertions(+), 29 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d9a37a4d45f..d3c1ff035f5 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -24,6 +24,7 @@ class ApplicationController < ActionController::Base helper_method :abilities, :can?, :current_application_settings helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled? + helper_method :repository rescue_from Encoding::CompatibilityError do |exception| log_exception(exception) diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index d22c7b550b0..6aa602321f4 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -65,8 +65,11 @@ class Projects::BlobController < Projects::ApplicationController end def diff - @form = UnfoldForm.new(params) - @lines = Gitlab::Diff::Highlight.process_diff_lines(@blob.name, @blob.data.lines[@form.since - 1..@form.to - 1]) + ref, file_name = params[:id].split('/', 2) + + @form = UnfoldForm.new(params) + @lines = Gitlab::Diff::Highlight.process_file(repository, ref, file_name) + @lines = @lines[@form.since - 1..@form.to - 1] if @form.bottom? @match_line = '' diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 0aaba3792bf..9bbf4581057 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -72,6 +72,7 @@ class Projects::CommitController < Projects::ApplicationController @diffs = commit.diffs end + @diff_refs = [commit.parent.id, commit.id] @notes_count = commit.notes.count @statuses = ci_commit.statuses if ci_commit diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 5200d609cc9..5b88aed7a64 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -20,6 +20,7 @@ class Projects::CompareController < Projects::ApplicationController if compare_result @commits = Commit.decorate(compare_result.commits, @project) @diffs = compare_result.diffs + @diff_refs = [base_ref, head_ref] @commit = @project.commit(head_ref) @first_commit = @project.commit(base_ref) @line_notes = [] diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index ab5c953189c..d25adbe952d 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -59,6 +59,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def diffs @commit = @merge_request.last_commit @first_commit = @merge_request.first_commit + @diff_refs = [@merge_request.target_sha, @merge_request.source_sha] @comments_allowed = @reply_allowed = true @comments_target = { @@ -103,6 +104,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commit = @merge_request.last_commit @first_commit = @merge_request.first_commit @diffs = @merge_request.compare_diffs + @diff_refs = [@merge_request.target_sha, @merge_request.source_branch] @ci_commit = @merge_request.ci_commit @statuses = @ci_commit.statuses if @ci_commit diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 22deb69cec5..668610364c5 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -19,13 +19,13 @@ module DiffHelper end end - def safe_diff_files(diffs) + def safe_diff_files(diffs, diff_refs, repository) lines = 0 safe_files = [] diffs.first(allowed_diff_size).each do |diff| lines += diff.diff.lines.count break if lines > allowed_diff_lines - safe_files << Gitlab::Diff::File.new(diff) + safe_files << Gitlab::Diff::File.new(diff, diff_refs, repository) end safe_files end diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 069b8b1f169..16ebce2d771 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -5,5 +5,6 @@ = render "ci_menu" - else %div.block-connector -= render "projects/diffs/diffs", diffs: @diffs, project: @project += render "projects/diffs/diffs", diffs: @diffs, project: @project, + diff_refs: @diff_refs = render "projects/notes/notes_with_form" diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index 51088a7dea8..da731f28bb6 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -9,7 +9,7 @@ - if @commits.present? .prepend-top-default = render "projects/commits/commit_list" - = render "projects/diffs/diffs", diffs: @diffs, project: @project + = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @diff_refs - else .light-well.prepend-top-default .center diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index f9d661d59d2..58ad7d79af9 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -1,7 +1,7 @@ - if diff_view == 'parallel' - fluid_layout true -- diff_files = safe_diff_files(diffs) +- diff_files = safe_diff_files(diffs, diff_refs, repository) .gray-content-block.middle-block.oneline-block .inline-parallel-buttons diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index a14943b15d3..0cb8b6daedb 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -38,7 +38,7 @@ = render "projects/merge_requests/show/commits" #diffs.diffs.tab-pane.active - if @diffs.present? - = render "projects/diffs/diffs", diffs: @diffs, project: @project + = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @diff_refs - elsif @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE .alert.alert-danger %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits. diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index d9cfc3d7ae9..1d9af1e4160 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -1,5 +1,5 @@ - if @merge_request_diff.collected? - = render "projects/diffs/diffs", diffs: params[:w] == '1' ? @merge_request.diffs_no_whitespace : @merge_request.diffs, project: @merge_request.project + = render "projects/diffs/diffs", diffs: params[:w] == '1' ? @merge_request.diffs_no_whitespace : @merge_request.diffs, project: @merge_request.project, diff_refs: @diff_refs - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} - else diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index 69b38a32eeb..cb93c6a574a 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -1,13 +1,15 @@ module Gitlab module Diff class File - attr_reader :diff + attr_reader :diff, :repository, :new_ref, :old_ref delegate :new_file, :deleted_file, :renamed_file, :old_path, :new_path, to: :diff, prefix: false - def initialize(diff) + def initialize(diff, diff_refs, repository) @diff = diff + @repository = repository + @old_ref, @new_ref = diff_refs end # Array of Gitlab::DIff::Line objects @@ -16,7 +18,7 @@ module Gitlab end def highlighted_diff_lines - Gitlab::Diff::Highlight.process_diff_lines(new_path, diff_lines) + Gitlab::Diff::Highlight.process_diff_lines(self) end def mode_changed? diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index 7f340de65cc..0d0a3268107 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -1,22 +1,43 @@ module Gitlab module Diff class Highlight + attr_reader :diff_file + + delegate :repository, :old_path, :new_path, :old_ref, :new_ref, + to: :diff_file, prefix: :diff + # Apply syntax highlight to provided source code # - # file_name - The file name related to the code. - # lines - It can be an Array of Gitlab::Diff::Line objects or simple Strings. - # When passing Strings you need to provide the required 'end of lines' - # chars ("\n") for each String given that we don't append them automatically. + # diff_file - an instance of Gitlab::Diff::File # # Returns an Array with the processed items. - def self.process_diff_lines(file_name, lines) - processor = new(file_name, lines) + def self.process_diff_lines(diff_file) + processor = new(diff_file) processor.highlight end - def initialize(file_name, lines) - @file_name = file_name - @lines = lines + def self.process_file(repository, ref, file_name) + blob = repository.blob_at(ref, file_name) + return [] unless blob + + content = blob.data + lexer = Rouge::Lexer.guess(filename: file_name, source: content).new rescue Rouge::Lexers::PlainText.new + formatter.format(lexer.lex(content)).lines + end + + def self.formatter + @formatter ||= Rouge::Formatters::HTMLGitlab.new( + nowrap: true, + cssclass: 'code highlight', + lineanchors: true, + lineanchorsid: 'LC' + ) + end + + def initialize(diff_file) + @diff_file = diff_file + @file_name = diff_file.new_path + @lines = diff_file.diff_lines end def highlight @@ -47,7 +68,7 @@ module Gitlab def extract_line_prefixes @diff_line_prefixes ||= begin if is_diff_line? - text_lines.map { |line| line.sub!(/\A((\+|\-)\s*)/, '');$1 } + text_lines.map { |line| line.sub!(/\A((\+|\-))/, '');$1 } else [] end @@ -57,11 +78,17 @@ module Gitlab def update_diff_lines @highlighted_code.lines.each_with_index do |line, i| diff_line = @lines[i] + line_prefix = @diff_line_prefixes[i] || ' ' # ignore highlighting for "match" lines next if diff_line.type == 'match' - diff_line.text = "#{@diff_line_prefixes[i]}#{line}" + case diff_line.type + when 'new', nil + diff_line.text = new_lines[diff_line.new_pos - 1].try(:gsub!, /\A\s/, line_prefix) + when 'old' + diff_line.text = old_lines[diff_line.old_pos - 1].try(:gsub!, /\A\s/, line_prefix) + end end @lines @@ -79,12 +106,21 @@ module Gitlab end def formatter - Rouge::Formatters::HTMLGitlab.new( - nowrap: true, - cssclass: 'code highlight', - lineanchors: true, - lineanchorsid: 'LC' - ) + self.class.formatter + end + + def old_lines + @old_lines ||= begin + lines = self.class.process_file(diff_repository, diff_old_ref, diff_old_path) + lines.map! { |line| " #{line}" } + end + end + + def new_lines + @new_lines ||= begin + lines = self.class.process_file(diff_repository, diff_new_ref, diff_new_path) + lines.map! { |line| " #{line}" } + end end end end -- cgit v1.2.1 From 6282202ee84f80f2197698b6f132abdf588e94d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Fri, 8 Jan 2016 15:17:45 -0500 Subject: Remove custom Lexer. #3945 [ci skip] Inline diff is going to be generated client side now. #3945 --- lib/gitlab/diff/highlight.rb | 3 +-- lib/rouge/lexers/gitlab_diff.rb | 26 -------------------------- 2 files changed, 1 insertion(+), 28 deletions(-) delete mode 100644 lib/rouge/lexers/gitlab_diff.rb diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index 0d0a3268107..7dd44b6004a 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -95,8 +95,7 @@ module Gitlab end def lexer - parent = Rouge::Lexer.guess(filename: @file_name, source: @code).new rescue Rouge::Lexers::PlainText.new - Rouge::Lexers::GitlabDiff.new(parent_lexer: parent) + Rouge::Lexer.guess(filename: @file_name, source: @code).new rescue Rouge::Lexers::PlainText.new end def unescape_html(content) diff --git a/lib/rouge/lexers/gitlab_diff.rb b/lib/rouge/lexers/gitlab_diff.rb deleted file mode 100644 index cbf272ee1de..00000000000 --- a/lib/rouge/lexers/gitlab_diff.rb +++ /dev/null @@ -1,26 +0,0 @@ -Rouge::Token::Tokens.token(:InlineDiff, 'idiff') - -module Rouge - module Lexers - # This new Lexer is required in order to avoid the inline diff markup - # to be tokenized, it will be rendered as raw HTML code if that happens. - class GitlabDiff < RegexLexer - title "GitLab Diff" - tag 'gitlab_diff' - - state :root do - rule %r{(.*?)} do |match| - token InlineDiff, match[1] - end - - rule /(?:(?! Date: Fri, 8 Jan 2016 18:40:05 -0500 Subject: Update specs. #3945 --- spec/lib/gitlab/diff/highlight_spec.rb | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index fc5cb894d2a..4ab509f47b9 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -6,24 +6,24 @@ describe Gitlab::Diff::Highlight, lib: true do let(:project) { create(:project) } let(:commit) { project.commit(sample_commit.id) } let(:diff) { commit.diffs.first } - let(:diff_file) { Gitlab::Diff::File.new(diff) } + let(:diff_file) { Gitlab::Diff::File.new(diff, [commit.parent.id, commit.id], project.repository) } describe '.process_diff_lines' do context 'when processing Gitlab::Diff::Line objects' do - let(:diff_lines) { Gitlab::Diff::Highlight.process_diff_lines(diff_file.new_path, diff_file.diff_lines) } + let(:diff_lines) { Gitlab::Diff::Highlight.process_diff_lines(diff_file) } it 'should return Gitlab::Diff::Line elements' do expect(diff_lines.first).to be_an_instance_of(Gitlab::Diff::Line) end it 'should highlight the code' do - code = %Q{ def popen(cmd, path=nil)\n} + code = %Q{ def popen(cmd, path=nil)\n} expect(diff_lines[2].text).to eq(code) end - it 'should keep the inline diff markup' do - expect(diff_lines[5].text).to match(Regexp.new(Regexp.escape('RuntimeError, '))) + it 'should not generate the inline diff markup' do + expect(diff_lines[5].text).not_to match(Regexp.new(Regexp.escape(''))) end it 'should not modify "match" lines' do @@ -31,19 +31,18 @@ describe Gitlab::Diff::Highlight, lib: true do expect(diff_lines[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') end end + end - context 'when processing raw lines' do - let(:lines) { ["puts 'Hello'\n", "# A comment"] } - let(:highlighted_lines) { Gitlab::Diff::Highlight.process_diff_lines('demo.rb', lines) } - - it 'should highlight the code' do - line_1 = %Q{puts 'Hello'\n} - line_2 = %Q{# A comment} - - expect(highlighted_lines[0]).to eq(line_1) - expect(highlighted_lines[1]).to eq(line_2) - end + describe '.process_file' do + let(:lines) do + Gitlab::Diff::Highlight.process_file(project.repository, commit.id, 'files/ruby/popen.rb') end + it 'should properly highlight all the lines' do + expect(lines[4]).to eq(%Q{ extend self\n}) + expect(lines[21]).to eq(%Q{ unless File.directory?(path)\n}) + expect(lines[26]).to eq(%Q{ @cmd_status = 0\n}) + end end + end -- cgit v1.2.1 From 78d7c0e0d81c40e0d9541e4ef0dd15df30d457e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Fri, 8 Jan 2016 19:05:55 -0500 Subject: Fix broken specs. #3945 --- app/views/projects/diffs/_parallel_view.html.haml | 4 ++-- lib/gitlab/diff/highlight.rb | 10 +++++----- spec/helpers/diff_helper_spec.rb | 3 ++- spec/lib/gitlab/diff/file_spec.rb | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 1ad54d1848b..77069501d1c 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -20,7 +20,7 @@ = link_to raw(line_number_left), "##{line_code_left}", id: line_code_left - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code_left, 'old') - %td.line_content{class: "parallel noteable_line #{type_left} #{line_code_left}", "line_code" => line_code_left }= line_content_left.html_safe + %td.line_content{class: "parallel noteable_line #{type_left} #{line_code_left}", "line_code" => line_code_left }= raw(line_content_left) - if type_right == 'new' - new_line_class = 'new' @@ -33,7 +33,7 @@ = link_to raw(line_number_right), "##{new_line_code}", id: new_line_code - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code_right, 'new') - %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", "line_code" => new_line_code}= line_content_right.html_safe + %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", "line_code" => new_line_code}= raw(line_content_right) - if @reply_allowed - comments_left, comments_right = organize_comments(type_left, type_right, line_code_left, line_code_right) diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index 7dd44b6004a..caeefe5bbdd 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -27,11 +27,11 @@ module Gitlab def self.formatter @formatter ||= Rouge::Formatters::HTMLGitlab.new( - nowrap: true, - cssclass: 'code highlight', - lineanchors: true, - lineanchorsid: 'LC' - ) + nowrap: true, + cssclass: 'code highlight', + lineanchors: true, + lineanchorsid: 'LC' + ) end def initialize(diff_file) diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 0501c2e8c29..84b8f4d1679 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -7,7 +7,8 @@ describe DiffHelper do let(:commit) { project.commit(sample_commit.id) } let(:diffs) { commit.diffs } let(:diff) { diffs.first } - let(:diff_file) { Gitlab::Diff::File.new(diff) } + let(:diff_refs) { [commit.parent.id, commit.id] } + let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs, project.repository) } describe 'diff_hard_limit_enabled?' do it 'should return true if param is provided' do diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index c7cdf8691d6..97ba7f14eb1 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Diff::File, lib: true do let(:project) { create(:project) } let(:commit) { project.commit(sample_commit.id) } let(:diff) { commit.diffs.first } - let(:diff_file) { Gitlab::Diff::File.new(diff) } + let(:diff_file) { Gitlab::Diff::File.new(diff, [commit.parent.id, commit.id], project.repository) } describe :diff_lines do let(:diff_lines) { diff_file.diff_lines } -- cgit v1.2.1 From 164c6374a708754086a1bca1033c46c01503056d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Fri, 8 Jan 2016 19:57:51 -0500 Subject: Fix broken specs. #3945 --- spec/fixtures/parallel_diff_result.yml | 156 ++++++++++++++++----------------- spec/helpers/diff_helper_spec.rb | 17 ++-- 2 files changed, 86 insertions(+), 87 deletions(-) diff --git a/spec/fixtures/parallel_diff_result.yml b/spec/fixtures/parallel_diff_result.yml index 16c4bac96df..020c1817226 100644 --- a/spec/fixtures/parallel_diff_result.yml +++ b/spec/fixtures/parallel_diff_result.yml @@ -1,86 +1,86 @@ --- - - match - 6 - - '@@ -6,12 +6,18 @@ module Popen' + - "@@ -6,12 +6,18 @@ module Popen" - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 - match - 6 - - '@@ -6,12 +6,18 @@ module Popen' + - "@@ -6,12 +6,18 @@ module Popen" - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 - - - 6 - - | - + - |2 + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 - - 6 - - | - + - |2 + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 - - - 7 - - | - def popen(cmd, path=nil) + - |2 + def popen(cmd, path=nil) - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7 - - 7 - - | - def popen(cmd, path=nil) + - |2 + def popen(cmd, path=nil) - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7 - - - 8 - - | - unless cmd.is_a?(Array) + - |2 + unless cmd.is_a?(Array) - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8 - - 8 - - | - unless cmd.is_a?(Array) + - |2 + unless cmd.is_a?(Array) - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8 - - old - 9 - | - - raise "System commands must be given as an array of strings" + - raise "System commands must be given as an array of strings" - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9 - new - 9 - | - + raise RuntimeError, "System commands must be given as an array of strings" + + raise RuntimeError, "System commands must be given as an array of strings" - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9 - - - 10 - - | - end + - |2 + end - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10 - - 10 - - | - end + - |2 + end - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10 - - - 11 - - | - + - |2 + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11 - - 11 - - | - + - |2 + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11 - - - 12 - - | - path ||= Dir.pwd + - |2 + path ||= Dir.pwd - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12 - - 12 - - | - path ||= Dir.pwd + - |2 + path ||= Dir.pwd - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12 - - old - 13 - | - - vars = { "PWD" => path } + - vars = { "PWD" => path } - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_13_13 - old - @@ -89,12 +89,12 @@ - - old - 14 - | - - options = { chdir: path } + - options = { chdir: path } - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_13 - new - 13 - | - + + + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_13 - - - @@ -103,7 +103,7 @@ - new - 14 - | - + vars = { + + vars = { - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14 - - - @@ -112,7 +112,7 @@ - new - 15 - | - + "PWD" => path + + "PWD" => path - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15 - - - @@ -121,7 +121,7 @@ - new - 16 - | - + } + + } - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16 - - - @@ -130,7 +130,7 @@ - new - 17 - | - + + + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17 - - - @@ -139,7 +139,7 @@ - new - 18 - | - + options = { + + options = { - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18 - - - @@ -148,7 +148,7 @@ - new - 19 - | - + chdir: path + + chdir: path - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19 - - - @@ -157,75 +157,75 @@ - new - 20 - | - + } + + } - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20 - - - 15 - - | - + - |2 + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21 - - 21 - - | - + - |2 + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21 - - - 16 - - | - unless File.directory?(path) + - |2 + unless File.directory?(path) - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22 - - 22 - - | - unless File.directory?(path) + - |2 + unless File.directory?(path) - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22 - - - 17 - - | - FileUtils.mkdir_p(path) + - |2 + FileUtils.mkdir_p(path) - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23 - - 23 - - | - FileUtils.mkdir_p(path) + - |2 + FileUtils.mkdir_p(path) - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23 - - match - 19 - - '@@ -19,6 +25,7 @@ module Popen' + - "@@ -19,6 +25,7 @@ module Popen" - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 - match - 25 - - '@@ -19,6 +25,7 @@ module Popen' + - "@@ -19,6 +25,7 @@ module Popen" - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 - - - 19 - - | - + - |2 + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 - - 25 - - | - + - |2 + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 - - - 20 - - | - @cmd_output = "" + - |2 + @cmd_output = "" - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26 - - 26 - - | - @cmd_output = "" + - |2 + @cmd_output = "" - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26 - - - 21 - - | - @cmd_status = 0 + - |2 + @cmd_status = 0 - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27 - - 27 - - | - @cmd_status = 0 + - |2 + @cmd_status = 0 - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27 - - - @@ -234,37 +234,35 @@ - new - 28 - | - + + + - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28 - - - 22 - - | - Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + - |2 + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29 - - 29 - - | - Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + - |2 + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29 - - - 23 - - | - @cmd_output << stdout.read + - |2 + @cmd_output << stdout.read - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30 - - 30 - - | - @cmd_output << stdout.read + - |2 + @cmd_output << stdout.read - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30 - - - 24 - - @cmd_output << stderr.read + - |2 + @cmd_output << stderr.read - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31 - - 31 - - @cmd_output << stderr.read + - |2 + @cmd_output << stderr.read - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31 diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 84b8f4d1679..4b10ee3d33c 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -4,11 +4,12 @@ describe DiffHelper do include RepoHelpers let(:project) { create(:project) } + let(:repository) { project.repository } let(:commit) { project.commit(sample_commit.id) } let(:diffs) { commit.diffs } let(:diff) { diffs.first } let(:diff_refs) { [commit.parent.id, commit.id] } - let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs, project.repository) } + let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs, repository) } describe 'diff_hard_limit_enabled?' do it 'should return true if param is provided' do @@ -45,41 +46,41 @@ describe DiffHelper do describe 'safe_diff_files' do it 'should return all files from a commit that is smaller than safe limits' do - expect(safe_diff_files(diffs).length).to eq(2) + expect(safe_diff_files(diffs, diff_refs, repository).length).to eq(2) end it 'should return only the first file if the diff line count in the 2nd file takes the total beyond safe limits' do allow(diffs[1].diff).to receive(:lines).and_return([""] * 4999) #simulate 4999 lines - expect(safe_diff_files(diffs).length).to eq(1) + expect(safe_diff_files(diffs, diff_refs, repository).length).to eq(1) end it 'should return all files from a commit that is beyond safe limit for numbers of lines if force diff is true' do allow(controller).to receive(:params) { { force_show_diff: true } } allow(diffs[1].diff).to receive(:lines).and_return([""] * 4999) #simulate 4999 lines - expect(safe_diff_files(diffs).length).to eq(2) + expect(safe_diff_files(diffs, diff_refs, repository).length).to eq(2) end it 'should return only the first file if the diff line count in the 2nd file takes the total beyond hard limits' do allow(controller).to receive(:params) { { force_show_diff: true } } allow(diffs[1].diff).to receive(:lines).and_return([""] * 49999) #simulate 49999 lines - expect(safe_diff_files(diffs).length).to eq(1) + expect(safe_diff_files(diffs, diff_refs, repository).length).to eq(1) end it 'should return only a safe number of file diffs if a commit touches more files than the safe limits' do large_diffs = diffs * 100 #simulate 200 diffs - expect(safe_diff_files(large_diffs).length).to eq(100) + expect(safe_diff_files(large_diffs, diff_refs, repository).length).to eq(100) end it 'should return all file diffs if a commit touches more files than the safe limits but force diff is true' do allow(controller).to receive(:params) { { force_show_diff: true } } large_diffs = diffs * 100 #simulate 200 diffs - expect(safe_diff_files(large_diffs).length).to eq(200) + expect(safe_diff_files(large_diffs, diff_refs, repository).length).to eq(200) end it 'should return a limited file diffs if a commit touches more files than the hard limits and force diff is true' do allow(controller).to receive(:params) { { force_show_diff: true } } very_large_diffs = diffs * 1000 #simulate 2000 diffs - expect(safe_diff_files(very_large_diffs).length).to eq(1000) + expect(safe_diff_files(very_large_diffs, diff_refs, repository).length).to eq(1000) end end -- cgit v1.2.1 From fed10766e533fcef2a6840deeb8d7ea1747f0c72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Sat, 9 Jan 2016 01:55:31 -0500 Subject: Fix broken spec for submodule commit. #3945 --- lib/gitlab/diff/highlight.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index caeefe5bbdd..3c44abff3fb 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -85,10 +85,14 @@ module Gitlab case diff_line.type when 'new', nil - diff_line.text = new_lines[diff_line.new_pos - 1].try(:gsub!, /\A\s/, line_prefix) + line = new_lines[diff_line.new_pos - 1] when 'old' - diff_line.text = old_lines[diff_line.old_pos - 1].try(:gsub!, /\A\s/, line_prefix) + line = old_lines[diff_line.old_pos - 1] end + + # Only update text if line is found. This will prevent + # issues with submodules given the line only exists in diff content. + diff_line.text = line.gsub!(/\A\s/, line_prefix) if line end @lines @@ -121,6 +125,10 @@ module Gitlab lines.map! { |line| " #{line}" } end end + + def submodules + @submodules ||= diff_repository.raw_repository.submodules(diff_new_ref).keys + end end end end -- cgit v1.2.1 From ee2230c3291354035f52cc4d87e3340982367fa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Sat, 9 Jan 2016 02:24:30 -0500 Subject: Fix css for other highlighting themes. #3945 --- app/assets/stylesheets/highlight/dark.scss | 4 ++++ app/assets/stylesheets/highlight/solarized_dark.scss | 4 ++++ app/assets/stylesheets/highlight/solarized_light.scss | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index f7d1334705f..8201735beb5 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -92,12 +92,16 @@ .il { color: #de935f } /* Literal.Number.Integer.Long */ .line_holder { + &.parallel .new.new_line, + &.parallel .new.line_content, &.new .old_line, &.new .new_line, &.new .line_content { @include diff_background(255, 255, 255, #808080); } + &.parallel .old.old_line, + &.parallel .old.line_content, &.old .old_line, &.old .new_line, &.old .line_content { diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index 2c3648274cf..fdfac6cd249 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -113,12 +113,16 @@ .il { color: #2aa198 } /* Literal.Number.Integer.Long */ .line_holder { + &.parallel .new.new_line, + &.parallel .new.line_content, &.new .old_line, &.new .new_line, &.new .line_content { @include diff_background(255, 255, 255, #808080); } + &.parallel .old.old_line, + &.parallel .old.line_content, &.old .old_line, &.old .new_line, &.old .line_content { diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index c16473ffe66..f9788951aa8 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -114,12 +114,16 @@ .line_holder { + &.parallel .new.new_line, + &.parallel .new.line_content, &.new .old_line, &.new .new_line, &.new .line_content { @include diff_background(92, 164, 169, #FAF3DD); } + &.parallel .old.old_line, + &.parallel .old.line_content, &.old .old_line, &.old .new_line, &.old .line_content { -- cgit v1.2.1 From 4b1023105a692b3f36f2a9c51fcf193b411156ed Mon Sep 17 00:00:00 2001 From: Jeroen Nijhof Date: Mon, 11 Jan 2016 17:11:22 +0100 Subject: Add housekeeping feature documentation --- doc/README.md | 1 + doc/administration/housekeeping.md | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 doc/administration/housekeeping.md diff --git a/doc/README.md b/doc/README.md index 25fe3abcb9a..5a05fbe50d3 100644 --- a/doc/README.md +++ b/doc/README.md @@ -67,6 +67,7 @@ - [Reply by email](incoming_email/README.md) Allow users to comment on issues and merge requests by replying to notification emails. - [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE. - [Git LFS configuration](workflow/lfs/lfs_administration.md) +- [Housekeeping](administration/housekeeping.md) Keep your git repository tidy and fast. ## Contributor documentation diff --git a/doc/administration/housekeeping.md b/doc/administration/housekeeping.md new file mode 100644 index 00000000000..9c4fb72fd73 --- /dev/null +++ b/doc/administration/housekeeping.md @@ -0,0 +1,13 @@ +# Housekeeping + +## Introduction + +The housekeeping function runs `git gc` on the current project git repository. + +`git gc` runs a number of housekeeping tasks, such as compressing file revisions (to reduce disk space and increase performance) and removing unreachable objects which may have been created from prior invocations of git add. + +Users are encouraged to run this task on a regular basis to maintain good disk space utilization and good operating performance. + +## Where can I find it? + +Just go to your project settings page and you will find the housekeeping function below the project settings form. -- cgit v1.2.1 From 80a4c808b1e5d331833f7b2ed531cb4fc81c7ef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 12 Jan 2016 11:02:16 -0500 Subject: Make diff_line_content helper return a safe String. #3945 --- app/helpers/diff_helper.rb | 4 ++-- app/views/projects/blob/preview.html.haml | 2 +- app/views/projects/diffs/_text_file.html.haml | 2 +- app/views/projects/notes/discussions/_diff.html.haml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 668610364c5..28c8e58b2ad 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -111,9 +111,9 @@ module DiffHelper def diff_line_content(line) if line.blank? - "  " + "  ".html_safe else - line + line.try(:html_safe) end end diff --git a/app/views/projects/blob/preview.html.haml b/app/views/projects/blob/preview.html.haml index e7c3460ad78..fed483d6788 100644 --- a/app/views/projects/blob/preview.html.haml +++ b/app/views/projects/blob/preview.html.haml @@ -20,6 +20,6 @@ - else %td.old_line %td.new_line - %td.line_content{class: "#{line.type}"}= raw diff_line_content(line.text) + %td.line_content{class: "#{line.type}"}= diff_line_content(line.text) - else .nothing-here-block No changes. diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index f1b8cba5e55..521f2ac1e8d 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -22,7 +22,7 @@ = link_to_new_diff_note(line_code) %td.new_line{data: {linenumber: line.new_pos}} = link_to raw(type == "old" ? " " : line.new_pos), "##{line_code}", id: line_code - %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= diff_line_content(line.text).html_safe + %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= diff_line_content(line.text) - if @reply_allowed - comments = @line_notes.select { |n| n.line_code == line_code && n.active? }.sort_by(&:created_at) diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml index 0301445b5b2..97347a9f67f 100644 --- a/app/views/projects/notes/discussions/_diff.html.haml +++ b/app/views/projects/notes/discussions/_diff.html.haml @@ -24,7 +24,7 @@ = raw(type == "new" ? " " : line.old_pos) %td.new_line = raw(type == "old" ? " " : line.new_pos) - %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw diff_line_content(line.text) + %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= diff_line_content(line.text) - if line_code == note.line_code = render "projects/notes/diff_notes_with_reply", notes: discussion_notes -- cgit v1.2.1 From c476395b4d8b78cfc7431153a144ffccbd414c61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 12 Jan 2016 11:14:41 -0500 Subject: Reuse existent vars with ref and path. #3945 --- app/controllers/projects/blob_controller.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 6aa602321f4..6ca3a636359 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -65,10 +65,8 @@ class Projects::BlobController < Projects::ApplicationController end def diff - ref, file_name = params[:id].split('/', 2) - @form = UnfoldForm.new(params) - @lines = Gitlab::Diff::Highlight.process_file(repository, ref, file_name) + @lines = Gitlab::Diff::Highlight.process_file(repository, @ref, @path) @lines = @lines[@form.since - 1..@form.to - 1] if @form.bottom? -- cgit v1.2.1 From f1f9b5f7d388c6d7a0938229c9211beddb2fd6a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 12 Jan 2016 12:53:54 -0500 Subject: Small fixes from code review. #3945 --- app/controllers/projects/merge_requests_controller.rb | 2 +- app/helpers/blob_helper.rb | 6 +++--- lib/gitlab/diff/highlight.rb | 4 ---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index d25adbe952d..b70533ef7be 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -104,7 +104,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commit = @merge_request.last_commit @first_commit = @merge_request.first_commit @diffs = @merge_request.compare_diffs - @diff_refs = [@merge_request.target_sha, @merge_request.source_branch] + @diff_refs = [@merge_request.target_sha, @merge_request.source_sha] @ci_commit = @merge_request.ci_commit @statuses = @ci_commit.statuses if @ci_commit diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 1230002e69c..a80162a3e33 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -11,14 +11,14 @@ module BlobHelper end def highlight(blob_name, blob_content, nowrap: false, continue: false) - @formatter ||= rouge_formatter(nowrap: nowrap) + formatter = rouge_formatter(nowrap: nowrap) begin @lexer ||= Rouge::Lexer.guess(filename: blob_name, source: blob_content).new - result = @formatter.format(@lexer.lex(blob_content, continue: continue)).html_safe + result = formatter.format(@lexer.lex(blob_content, continue: continue)).html_safe rescue @lexer = Rouge::Lexers::PlainText - result = @formatter.format(@lexer.lex(blob_content)).html_safe + result = formatter.format(@lexer.lex(blob_content)).html_safe end result diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index 3c44abff3fb..d0137ab5f08 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -125,10 +125,6 @@ module Gitlab lines.map! { |line| " #{line}" } end end - - def submodules - @submodules ||= diff_repository.raw_repository.submodules(diff_new_ref).keys - end end end end -- cgit v1.2.1 From c0385488fbfaf1285122793cea417e607c8e771e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 12 Jan 2016 15:12:30 -0500 Subject: Fix broken spec. #3945 --- app/helpers/diff_helper.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 28c8e58b2ad..0ec532a9a90 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -113,7 +113,8 @@ module DiffHelper if line.blank? "  ".html_safe else - line.try(:html_safe) + # Return line if it isn't a String, it helps when it's Numeric + line.is_a?(String) ? line.html_safe : line end end -- cgit v1.2.1 From 6e3358a5077f5c6052e733722cd6baa63e43c081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 12 Jan 2016 17:36:08 -0500 Subject: Remove no longer required code. #3945 --- lib/gitlab/diff/highlight.rb | 51 ++++++++------------------------------------ 1 file changed, 9 insertions(+), 42 deletions(-) diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index d0137ab5f08..14cf98e24a2 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -44,74 +44,41 @@ module Gitlab return [] if @lines.empty? extract_line_prefixes - - @code = unescape_html(raw_content) - @highlighted_code = formatter.format(lexer.lex(@code)) - - is_diff_line? ? update_diff_lines : @highlighted_code.lines + update_diff_lines end private - def is_diff_line? - @lines.first.is_a?(Gitlab::Diff::Line) - end - def text_lines - @text_lines ||= (is_diff_line? ? @lines.map(&:text) : @lines) - end - - def raw_content - @raw_content ||= text_lines.join(is_diff_line? ? "\n" : nil) + @text_lines ||= @lines.map(&:text) end def extract_line_prefixes - @diff_line_prefixes ||= begin - if is_diff_line? - text_lines.map { |line| line.sub!(/\A((\+|\-))/, '');$1 } - else - [] - end - end + @diff_line_prefixes ||= text_lines.map { |line| line.sub!(/\A((\+|\-))/, '');$1 } end def update_diff_lines - @highlighted_code.lines.each_with_index do |line, i| - diff_line = @lines[i] + @lines.each_with_index do |line, i| line_prefix = @diff_line_prefixes[i] || ' ' # ignore highlighting for "match" lines - next if diff_line.type == 'match' + next if line.type == 'match' - case diff_line.type + case line.type when 'new', nil - line = new_lines[diff_line.new_pos - 1] + highlighted_line = new_lines[line.new_pos - 1] when 'old' - line = old_lines[diff_line.old_pos - 1] + highlighted_line = old_lines[line.old_pos - 1] end # Only update text if line is found. This will prevent # issues with submodules given the line only exists in diff content. - diff_line.text = line.gsub!(/\A\s/, line_prefix) if line + line.text = highlighted_line.gsub!(/\A\s/, line_prefix) if line end @lines end - def lexer - Rouge::Lexer.guess(filename: @file_name, source: @code).new rescue Rouge::Lexers::PlainText.new - end - - def unescape_html(content) - text = CGI.unescapeHTML(content) - text.gsub!(' ', ' ') - text - end - - def formatter - self.class.formatter - end - def old_lines @old_lines ||= begin lines = self.class.process_file(diff_repository, diff_old_ref, diff_old_path) -- cgit v1.2.1 From f547e733d1f8acf2c8ae82835b91ae166cf95b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 12 Jan 2016 17:49:11 -0500 Subject: Add more specs. #3945 --- spec/lib/gitlab/diff/highlight_spec.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index 4ab509f47b9..cdeed603e23 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -30,6 +30,24 @@ describe Gitlab::Diff::Highlight, lib: true do expect(diff_lines[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') expect(diff_lines[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') end + + it 'should highlight unchanged lines' do + code = %Q{ def popen(cmd, path=nil)\n} + + expect(diff_lines[2].text).to eq(code) + end + + it 'should highlight added lines' do + code = %Q{+ raise RuntimeError, "System commands must be given as an array of strings"\n} + + expect(diff_lines[5].text).to eq(code) + end + + it 'should highlight removed lines' do + code = %Q{- raise "System commands must be given as an array of strings"\n} + + expect(diff_lines[4].text).to eq(code) + end end end -- cgit v1.2.1 From 7307fa48e70a9fb42902c8a84479d4cf669640aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 12 Jan 2016 19:28:31 -0500 Subject: Fix broken specs. #3945 --- app/controllers/projects/merge_requests_controller.rb | 3 ++- lib/gitlab/diff/highlight.rb | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index b70533ef7be..95fd62b8ace 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -104,7 +104,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commit = @merge_request.last_commit @first_commit = @merge_request.first_commit @diffs = @merge_request.compare_diffs - @diff_refs = [@merge_request.target_sha, @merge_request.source_sha] + # We need to use #source_branch because #source_sha requires an existent MergeRequestDiff object. + @diff_refs = [@merge_request.target_sha, @merge_request.source_branch] @ci_commit = @merge_request.ci_commit @statuses = @ci_commit.statuses if @ci_commit diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index 14cf98e24a2..0b6a348acbc 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -73,7 +73,7 @@ module Gitlab # Only update text if line is found. This will prevent # issues with submodules given the line only exists in diff content. - line.text = highlighted_line.gsub!(/\A\s/, line_prefix) if line + line.text = highlighted_line.gsub!(/\A\s/, line_prefix) if highlighted_line end @lines -- cgit v1.2.1 From a42fe49c8c953c0aa8ca20c8fb5a141447128894 Mon Sep 17 00:00:00 2001 From: Jeroen Nijhof Date: Wed, 13 Jan 2016 12:07:05 +0100 Subject: Replaced user recommendation with link to git-gc documentation --- doc/administration/housekeeping.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/administration/housekeeping.md b/doc/administration/housekeeping.md index 9c4fb72fd73..b90f916fea9 100644 --- a/doc/administration/housekeeping.md +++ b/doc/administration/housekeeping.md @@ -6,7 +6,7 @@ The housekeeping function runs `git gc` on the current project git repository. `git gc` runs a number of housekeeping tasks, such as compressing file revisions (to reduce disk space and increase performance) and removing unreachable objects which may have been created from prior invocations of git add. -Users are encouraged to run this task on a regular basis to maintain good disk space utilization and good operating performance. +See https://www.kernel.org/pub/software/scm/git/docs/git-gc.html for details. ## Where can I find it? -- cgit v1.2.1 From 48c45ba9a8a9a5536a3d501e40536cc5b73062a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 13 Jan 2016 10:58:19 -0500 Subject: Use html_safe instead of raw. #3945 --- app/views/projects/diffs/_parallel_view.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 77069501d1c..1ad54d1848b 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -20,7 +20,7 @@ = link_to raw(line_number_left), "##{line_code_left}", id: line_code_left - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code_left, 'old') - %td.line_content{class: "parallel noteable_line #{type_left} #{line_code_left}", "line_code" => line_code_left }= raw(line_content_left) + %td.line_content{class: "parallel noteable_line #{type_left} #{line_code_left}", "line_code" => line_code_left }= line_content_left.html_safe - if type_right == 'new' - new_line_class = 'new' @@ -33,7 +33,7 @@ = link_to raw(line_number_right), "##{new_line_code}", id: new_line_code - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code_right, 'new') - %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", "line_code" => new_line_code}= raw(line_content_right) + %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", "line_code" => new_line_code}= line_content_right.html_safe - if @reply_allowed - comments_left, comments_right = organize_comments(type_left, type_right, line_code_left, line_code_right) -- cgit v1.2.1 From 0f0af19139db71255934e9a7a5b5cd86420b7186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 13 Jan 2016 11:39:15 -0500 Subject: Little refactor for usage of html_safe. #3945 --- app/helpers/diff_helper.rb | 9 +++++---- app/views/projects/blob/diff.html.haml | 2 +- app/views/projects/diffs/_parallel_view.html.haml | 4 ++-- lib/gitlab/diff/highlight.rb | 4 ++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 0ec532a9a90..d49e22e8c84 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -1,4 +1,6 @@ module DiffHelper + BLANK_SPACE = " ".html_safe + def diff_view params[:view] == 'parallel' ? 'parallel' : 'inline' end @@ -83,7 +85,7 @@ module DiffHelper elsif next_type == 'old' || next_type.nil? # Left side has text removed, right side doesn't have any change # No next line code, no new line number, no new line text - line = [type, line_old, full_line, line_code, next_type, nil, " ", nil] + line = [type, line_old, full_line, line_code, next_type, nil, BLANK_SPACE, nil] lines.push(line) end elsif type == 'new' @@ -93,7 +95,7 @@ module DiffHelper next else # Change is only on the right side, left side has no change - line = [nil, nil, " ", line_code, type, line_new, full_line, line_code] + line = [nil, nil, BLANK_SPACE, line_code, type, line_new, full_line, line_code] lines.push(line) end end @@ -113,8 +115,7 @@ module DiffHelper if line.blank? "  ".html_safe else - # Return line if it isn't a String, it helps when it's Numeric - line.is_a?(String) ? line.html_safe : line + line.html_safe end end diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index ef23876677a..2e913802be1 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -11,7 +11,7 @@ %td.old_line.diff-line-num{data: {linenumber: line_old}} = link_to raw(line_old), "#" %td.new_line= link_to raw(line_new) , "#" - %td.line_content.noteable_line= "#{' ' * @form.indent}#{line}".html_safe + %td.line_content.noteable_line==#{' ' * @form.indent}#{line} - if @form.unfold? && @form.bottom? && @form.to < @blob.loc %tr.line_holder{ id: @form.to } diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 1ad54d1848b..e9108c04cef 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -20,7 +20,7 @@ = link_to raw(line_number_left), "##{line_code_left}", id: line_code_left - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code_left, 'old') - %td.line_content{class: "parallel noteable_line #{type_left} #{line_code_left}", "line_code" => line_code_left }= line_content_left.html_safe + %td.line_content{class: "parallel noteable_line #{type_left} #{line_code_left}", "line_code" => line_code_left }= line_content_left - if type_right == 'new' - new_line_class = 'new' @@ -33,7 +33,7 @@ = link_to raw(line_number_right), "##{new_line_code}", id: new_line_code - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code_right, 'new') - %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", "line_code" => new_line_code}= line_content_right.html_safe + %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", "line_code" => new_line_code}= line_content_right - if @reply_allowed - comments_left, comments_right = organize_comments(type_left, type_right, line_code_left, line_code_right) diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index 0b6a348acbc..f940b57d596 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -22,7 +22,7 @@ module Gitlab content = blob.data lexer = Rouge::Lexer.guess(filename: file_name, source: content).new rescue Rouge::Lexers::PlainText.new - formatter.format(lexer.lex(content)).lines + formatter.format(lexer.lex(content)).lines.map!(&:html_safe) end def self.formatter @@ -73,7 +73,7 @@ module Gitlab # Only update text if line is found. This will prevent # issues with submodules given the line only exists in diff content. - line.text = highlighted_line.gsub!(/\A\s/, line_prefix) if highlighted_line + line.text = highlighted_line.gsub!(/\A\s/, line_prefix).html_safe if highlighted_line end @lines -- cgit v1.2.1 From 1161cf2ec610cb0ceba61c92180566c9786ab059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 13 Jan 2016 18:01:40 -0500 Subject: Use current commit id if it doesn't have a parent. #3945 --- app/controllers/projects/commit_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 9bbf4581057..c8f143dd6b4 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -72,7 +72,7 @@ class Projects::CommitController < Projects::ApplicationController @diffs = commit.diffs end - @diff_refs = [commit.parent.id, commit.id] + @diff_refs = [commit.parent_id || commit.id, commit.id] @notes_count = commit.notes.count @statuses = ci_commit.statuses if ci_commit -- cgit v1.2.1 From c179b48c97be22ef55ef9f5874984f7359fb12f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 13 Jan 2016 21:04:53 -0500 Subject: Use #sub instead of #gsub!. #3945 * This is because is not a good idea to modify the original lines * Also I run into this issue https://gitlab.com/gitlab-org/gitlab_git/issues/14 which is returning Diff Lines with the same @new_pos value. --- lib/gitlab/diff/highlight.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index f940b57d596..e76a6f27856 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -73,7 +73,7 @@ module Gitlab # Only update text if line is found. This will prevent # issues with submodules given the line only exists in diff content. - line.text = highlighted_line.gsub!(/\A\s/, line_prefix).html_safe if highlighted_line + line.text = highlighted_line.sub(/\A\s/, line_prefix).html_safe if highlighted_line end @lines -- cgit v1.2.1 From c881627d114eb9c050d605e93673ef65a9da9a58 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 14 Jan 2016 14:52:08 +0100 Subject: Refactor parallel_diff generation a bit. --- app/helpers/blob_helper.rb | 11 +-- app/helpers/diff_helper.rb | 86 +++++++++++++++++------ app/views/projects/diffs/_parallel_view.html.haml | 43 +++++------- 3 files changed, 83 insertions(+), 57 deletions(-) diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index a80162a3e33..84e3cbb380b 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -13,15 +13,8 @@ module BlobHelper def highlight(blob_name, blob_content, nowrap: false, continue: false) formatter = rouge_formatter(nowrap: nowrap) - begin - @lexer ||= Rouge::Lexer.guess(filename: blob_name, source: blob_content).new - result = formatter.format(@lexer.lex(blob_content, continue: continue)).html_safe - rescue - @lexer = Rouge::Lexers::PlainText - result = formatter.format(@lexer.lex(blob_content)).html_safe - end - - result + @lexer ||= Rouge::Lexer.guess(filename: blob_name, source: blob_content).new rescue Rouge::Lexers::PlainText + formatter.format(@lexer.lex(blob_content, continue: continue)).html_safe end def no_highlight_files diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index d49e22e8c84..1596f9e7d19 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -1,6 +1,4 @@ module DiffHelper - BLANK_SPACE = " ".html_safe - def diff_view params[:view] == 'parallel' ? 'parallel' : 'inline' end @@ -49,15 +47,7 @@ module DiffHelper lines = [] skip_next = false - # Building array of lines - # - # [ - # left_type, left_line_number, left_line_content, left_line_code, - # right_line_type, right_line_number, right_line_content, right_line_code - # ] - # diff_file.highlighted_diff_lines.each do |line| - full_line = line.text type = line.type line_code = generate_line_code(diff_file.file_path, line) @@ -72,31 +62,81 @@ module DiffHelper next_line = next_line.text end - if type == 'match' || type.nil? + case type + when 'match', nil # line in the right panel is the same as in the left one - line = [type, line_old, full_line, line_code, type, line_new, full_line, line_code] - lines.push(line) - elsif type == 'old' - if next_type == 'new' + lines << { + left: { + type: type, + number: line_old, + text: full_line, + line_code: line_code, + }, + right: { + type: type, + number: line_new, + text: full_line, + line_code: line_code + } + } + when 'old' + case next_type + when 'new' # Left side has text removed, right side has text added - line = [type, line_old, full_line, line_code, next_type, line_new, next_line, next_line_code] - lines.push(line) + lines << { + left: { + type: type, + number: line_old, + text: full_line, + line_code: line_code, + }, + right: { + type: next_type, + number: line_new, + text: next_line, + line_code: next_line_code + } + } skip_next = true - elsif next_type == 'old' || next_type.nil? + when 'old', nil # Left side has text removed, right side doesn't have any change # No next line code, no new line number, no new line text - line = [type, line_old, full_line, line_code, next_type, nil, BLANK_SPACE, nil] - lines.push(line) + lines << { + left: { + type: type, + number: line_old, + text: full_line, + line_code: line_code, + }, + right: { + type: next_type, + number: nil, + text: "", + line_code: nil + } + } end - elsif type == 'new' + when 'new' if skip_next # Change has been already included in previous line so no need to do it again skip_next = false next else # Change is only on the right side, left side has no change - line = [nil, nil, BLANK_SPACE, line_code, type, line_new, full_line, line_code] - lines.push(line) + lines << { + left: { + type: nil, + number: nil, + text: "", + line_code: line_code, + }, + right: { + type: type, + number: line_new, + text: full_line, + line_code: line_code + } + } end end end diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index e9108c04cef..a2958286ada 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -2,41 +2,34 @@ %div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight %table - parallel_diff(diff_file, index).each do |line| - - type_left = line[0] - - line_number_left = line[1] - - line_content_left = line[2] - - line_code_left = line[3] - - type_right = line[4] - - line_number_right = line[5] - - line_content_right = line[6] - - line_code_right = line[7] - + - left = line[:left] + - right = line[:right] %tr.line_holder.parallel - - if type_left == 'match' - = render "projects/diffs/match_line_parallel", { line: line_content_left, - line_old: line_number_left, line_new: line_number_right } - - elsif type_left == 'old' || type_left.nil? - %td.old_line{id: line_code_left, class: "#{type_left}"} - = link_to raw(line_number_left), "##{line_code_left}", id: line_code_left + - if left[:type] == 'match' + = render "projects/diffs/match_line_parallel", { line: left[:text], + line_old: left[:number], line_new: right[:number] } + - else + %td.old_line{id: left[:line_code], class: "#{left[:type]}"} + = link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code] - if @comments_allowed && can?(current_user, :create_note, @project) - = link_to_new_diff_note(line_code_left, 'old') - %td.line_content{class: "parallel noteable_line #{type_left} #{line_code_left}", "line_code" => line_code_left }= line_content_left + = link_to_new_diff_note(left[:line_code], 'old') + %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]}", "line_code" => left[:line_code] }= diff_line_content(left[:text]) - - if type_right == 'new' + - if right[:type] == 'new' - new_line_class = 'new' - - new_line_code = line_code_right + - new_line_code = right[:line_code] - else - new_line_class = nil - - new_line_code = line_code_left + - new_line_code = left[:line_code] - %td.new_line{id: new_line_code, class: "#{new_line_class}", data: { linenumber: line_number_right }} - = link_to raw(line_number_right), "##{new_line_code}", id: new_line_code + %td.new_line{id: new_line_code, class: "#{new_line_class}", data: { linenumber: right[:number] }} + = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code - if @comments_allowed && can?(current_user, :create_note, @project) - = link_to_new_diff_note(line_code_right, 'new') - %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", "line_code" => new_line_code}= line_content_right + = link_to_new_diff_note(right[:line_code], 'new') + %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", "line_code" => new_line_code}= diff_line_content(right[:text]) - if @reply_allowed - - comments_left, comments_right = organize_comments(type_left, type_right, line_code_left, line_code_right) + - comments_left, comments_right = organize_comments(left[:type], right[:type], left[:line_code], right[:line_code]) - if comments_left.present? || comments_right.present? = render "projects/notes/diff_notes_with_reply_parallel", notes_left: comments_left, notes_right: comments_right -- cgit v1.2.1 From 3a1d0535992594bc77320f081d1f20b760b1c1f7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 14 Jan 2016 15:11:50 +0100 Subject: Remove duplication around highlighting. --- app/helpers/blob_helper.rb | 16 +--------------- lib/gitlab/diff/highlight.rb | 13 +------------ lib/gitlab/highlight.rb | 23 +++++++++++++++++++++++ 3 files changed, 25 insertions(+), 27 deletions(-) create mode 100644 lib/gitlab/highlight.rb diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 84e3cbb380b..be856242c43 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -1,20 +1,6 @@ module BlobHelper - def rouge_formatter(options = {}) - default_options = { - nowrap: false, - cssclass: 'code highlight', - lineanchors: true, - lineanchorsid: 'LC' - } - - Rouge::Formatters::HTMLGitlab.new(default_options.merge!(options)) - end - def highlight(blob_name, blob_content, nowrap: false, continue: false) - formatter = rouge_formatter(nowrap: nowrap) - - @lexer ||= Rouge::Lexer.guess(filename: blob_name, source: blob_content).new rescue Rouge::Lexers::PlainText - formatter.format(@lexer.lex(blob_content, continue: continue)).html_safe + Gitlab::Highlight.highlight(blob_name, blob_content, nowrap: nowrap, continue: continue) end def no_highlight_files diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index e76a6f27856..f34eff62d79 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -20,18 +20,7 @@ module Gitlab blob = repository.blob_at(ref, file_name) return [] unless blob - content = blob.data - lexer = Rouge::Lexer.guess(filename: file_name, source: content).new rescue Rouge::Lexers::PlainText.new - formatter.format(lexer.lex(content)).lines.map!(&:html_safe) - end - - def self.formatter - @formatter ||= Rouge::Formatters::HTMLGitlab.new( - nowrap: true, - cssclass: 'code highlight', - lineanchors: true, - lineanchorsid: 'LC' - ) + Gitlab::Highlight.highlight(file_name, blob.data).lines.map!(&:html_safe) end def initialize(diff_file) diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb new file mode 100644 index 00000000000..02e097eca3d --- /dev/null +++ b/lib/gitlab/highlight.rb @@ -0,0 +1,23 @@ +module Gitlab + class Highlight + def self.highlight(blob_name, blob_content, nowrap: true, continue: false) + formatter = rouge_formatter(nowrap: nowrap) + + lexer = Rouge::Lexer.guess(filename: blob_name, source: blob_content).new rescue Rouge::Lexers::PlainText + formatter.format(lexer.lex(blob_content, continue: continue)).html_safe + end + + private + + def self.rouge_formatter(options = {}) + options = options.reverse_merge( + nowrap: true, + cssclass: 'code highlight', + lineanchors: true, + lineanchorsid: 'LC' + ) + + Rouge::Formatters::HTMLGitlab.new(options) + end + end +end -- cgit v1.2.1 From 83e4fc188b22731d89106b4da28f11bf5509c116 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 14 Jan 2016 16:13:35 +0100 Subject: Refactor highlighting lines --- lib/gitlab/diff/highlight.rb | 29 ++++------------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index f34eff62d79..ba2f12db147 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -32,23 +32,8 @@ module Gitlab def highlight return [] if @lines.empty? - extract_line_prefixes - update_diff_lines - end - - private - - def text_lines - @text_lines ||= @lines.map(&:text) - end - - def extract_line_prefixes - @diff_line_prefixes ||= text_lines.map { |line| line.sub!(/\A((\+|\-))/, '');$1 } - end - - def update_diff_lines @lines.each_with_index do |line, i| - line_prefix = @diff_line_prefixes[i] || ' ' + line_prefix = line.text.match(/\A([+-])/) ? $1 : ' ' # ignore highlighting for "match" lines next if line.type == 'match' @@ -62,24 +47,18 @@ module Gitlab # Only update text if line is found. This will prevent # issues with submodules given the line only exists in diff content. - line.text = highlighted_line.sub(/\A\s/, line_prefix).html_safe if highlighted_line + line.text = highlighted_line.insert(0, line_prefix).html_safe if highlighted_line end @lines end def old_lines - @old_lines ||= begin - lines = self.class.process_file(diff_repository, diff_old_ref, diff_old_path) - lines.map! { |line| " #{line}" } - end + @old_lines ||= self.class.process_file(diff_repository, diff_old_ref, diff_old_path) end def new_lines - @new_lines ||= begin - lines = self.class.process_file(diff_repository, diff_new_ref, diff_new_path) - lines.map! { |line| " #{line}" } - end + @new_lines ||= self.class.process_file(diff_repository, diff_new_ref, diff_new_path) end end end -- cgit v1.2.1 From 8dfad143d44af4896ff6c71e8a42ad32b69ad593 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 14 Jan 2016 22:28:07 +0100 Subject: Add inline diff markers in highlighted diffs. --- app/controllers/projects/blob_controller.rb | 2 +- app/views/projects/diffs/_parallel_view.html.haml | 4 +- app/views/projects/diffs/_text_file.html.haml | 2 +- lib/gitlab/diff/file.rb | 2 +- lib/gitlab/diff/highlight.rb | 206 ++++++++++++++++++---- lib/gitlab/highlight.rb | 7 + spec/lib/gitlab/diff/highlight_spec.rb | 4 +- 7 files changed, 187 insertions(+), 40 deletions(-) diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 6ca3a636359..8133de90a41 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -66,7 +66,7 @@ class Projects::BlobController < Projects::ApplicationController def diff @form = UnfoldForm.new(params) - @lines = Gitlab::Diff::Highlight.process_file(repository, @ref, @path) + @lines = Gitlab::Highlight.highlight_lines(repository, @ref, @path) @lines = @lines[@form.since - 1..@form.to - 1] if @form.bottom? diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index a2958286ada..7b9cecbc3da 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -13,7 +13,7 @@ = link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code] - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(left[:line_code], 'old') - %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]}", "line_code" => left[:line_code] }= diff_line_content(left[:text]) + %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]}", data: { "line_code" => left[:line_code] }}= diff_line_content(left[:text]) - if right[:type] == 'new' - new_line_class = 'new' @@ -26,7 +26,7 @@ = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(right[:line_code], 'new') - %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", "line_code" => new_line_code}= diff_line_content(right[:text]) + %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", data: { "line_code" => new_line_code }}= diff_line_content(right[:text]) - if @reply_allowed - comments_left, comments_right = organize_comments(left[:type], right[:type], left[:line_code], right[:line_code]) diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index 521f2ac1e8d..6761155dcf9 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -22,7 +22,7 @@ = link_to_new_diff_note(line_code) %td.new_line{data: {linenumber: line.new_pos}} = link_to raw(type == "old" ? " " : line.new_pos), "##{line_code}", id: line_code - %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= diff_line_content(line.text) + %td.line_content{class: "noteable_line #{type} #{line_code}", data: { "line_code" => line_code }}= diff_line_content(line.text) - if @reply_allowed - comments = @line_notes.select { |n| n.line_code == line_code && n.active? }.sort_by(&:created_at) diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index cb93c6a574a..c1a6e16da5a 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -18,7 +18,7 @@ module Gitlab end def highlighted_diff_lines - Gitlab::Diff::Highlight.process_diff_lines(self) + Gitlab::Diff::Highlight.new(self).highlight end def mode_changed? diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index ba2f12db147..e21f496102d 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -6,59 +6,199 @@ module Gitlab delegate :repository, :old_path, :new_path, :old_ref, :new_ref, to: :diff_file, prefix: :diff - # Apply syntax highlight to provided source code - # - # diff_file - an instance of Gitlab::Diff::File - # - # Returns an Array with the processed items. - def self.process_diff_lines(diff_file) - processor = new(diff_file) - processor.highlight + def initialize(diff_file) + @diff_file = diff_file + @diff_lines = diff_file.diff_lines + @raw_lines = @diff_lines.map(&:text) end - def self.process_file(repository, ref, file_name) - blob = repository.blob_at(ref, file_name) - return [] unless blob + def highlight + return [] if @diff_lines.empty? - Gitlab::Highlight.highlight(file_name, blob.data).lines.map!(&:html_safe) - end + find_inline_diffs - def initialize(diff_file) - @diff_file = diff_file - @file_name = diff_file.new_path - @lines = diff_file.diff_lines + process_lines + + @diff_lines end - def highlight - return [] if @lines.empty? + private - @lines.each_with_index do |line, i| - line_prefix = line.text.match(/\A([+-])/) ? $1 : ' ' + def find_inline_diffs + @inline_diffs = [] + local_edit_indexes.each do |index| + old_index = index + new_index = index + 1 + old_line = @raw_lines[old_index][1..-1] + new_line = @raw_lines[new_index][1..-1] + + # Skip inline diff if empty line was replaced with content + next if old_line == "" + + lcp = longest_common_prefix(old_line, new_line) + lcs = longest_common_suffix(old_line, new_line) + + old_diff_range = lcp..(old_line.length - lcs - 1) + new_diff_range = lcp..(new_line.length - lcs - 1) + + @inline_diffs[old_index] = old_diff_range if old_diff_range.begin <= old_diff_range.end + @inline_diffs[new_index] = new_diff_range if new_diff_range.begin <= new_diff_range.end + end + end + + def process_lines + @diff_lines.each_with_index do |diff_line, i| # ignore highlighting for "match" lines - next if line.type == 'match' + next if diff_line.type == 'match' + + rich_line = highlight_line(diff_line, i) + rich_line = mark_inline_diffs(rich_line, diff_line, i) + diff_line.text = rich_line.html_safe + end + end + + def highlight_line(diff_line, index) + line_prefix = line_prefixes[index] + + case diff_line.type + when 'new', nil + rich_line = new_lines[diff_line.new_pos - 1] + when 'old' + rich_line = old_lines[diff_line.old_pos - 1] + end + + # Only update text if line is found. This will prevent + # issues with submodules given the line only exists in diff content. + rich_line ? line_prefix + rich_line : diff_line.text + end + + def mark_inline_diffs(rich_line, diff_line, index) + inline_diff = @inline_diffs[index] + return rich_line unless inline_diff + + raw_line = diff_line.text + + # Based on the prefixless versions + from = inline_diff.begin + 1 + to = inline_diff.end + 1 + + position_mapping = map_character_positions(raw_line, rich_line) + inline_diff_positions = position_mapping[from..to] + marker_ranges = collapse_ranges(inline_diff_positions) + + offset = 0 + marker_ranges.each do |range| + offset = insert_around_range(rich_line, range, "", "", offset) + end + + rich_line + end - case line.type - when 'new', nil - highlighted_line = new_lines[line.new_pos - 1] - when 'old' - highlighted_line = old_lines[line.old_pos - 1] + def line_prefixes + @line_prefixes ||= @raw_lines.map { |line| line.match(/\A([+-])/) ? $1 : ' ' } + end + + def local_edit_indexes + @local_edit_indexes ||= begin + joined_line_prefixes = " #{line_prefixes.join} " + + offset = 0 + local_edit_indexes = [] + while index = joined_line_prefixes.index(" -+ ", offset) + local_edit_indexes << index + offset = index + 1 end - # Only update text if line is found. This will prevent - # issues with submodules given the line only exists in diff content. - line.text = highlighted_line.insert(0, line_prefix).html_safe if highlighted_line + local_edit_indexes end + end + + def map_character_positions(raw_line, rich_line) + mapping = [] + raw_pos = 0 + rich_pos = 0 + (0..raw_line.length).each do |raw_pos| + raw_char = raw_line[raw_pos] + rich_char = rich_line[rich_pos] + + while rich_char == '<' + until rich_char == '>' + rich_pos += 1 + rich_char = rich_line[rich_pos] + end + + rich_pos += 1 + rich_char = rich_line[rich_pos] + end - @lines + mapping[raw_pos] = rich_pos + + rich_pos += 1 + end + + mapping end def old_lines - @old_lines ||= self.class.process_file(diff_repository, diff_old_ref, diff_old_path) + @old_lines ||= Gitlab::Highlight.highlight_lines(diff_repository, diff_old_ref, diff_old_path) end def new_lines - @new_lines ||= self.class.process_file(diff_repository, diff_new_ref, diff_new_path) + @new_lines ||= Gitlab::Highlight.highlight_lines(diff_repository, diff_new_ref, diff_new_path) + end + + def longest_common_suffix(a, b) + longest_common_prefix(a.reverse, b.reverse) + end + + def longest_common_prefix(a, b) + max_length = [a.length, b.length].max + + length = 0 + (0..max_length - 1).each do |pos| + old_char = a[pos] + new_char = b[pos] + + break if old_char != new_char + length += 1 + end + + length + end + + def collapse_ranges(positions) + return [] if positions.empty? + ranges = [] + + start = prev = positions[0] + range = start..prev + positions[1..-1].each do |pos| + if pos == prev + 1 + range = start..pos + prev = pos + else + ranges << range + start = prev = pos + range = start..prev + end + end + ranges << range + + ranges + end + + def insert_around_range(text, range, before, after, offset = 0) + from = range.begin + to = range.end + + text.insert(offset + from, before) + offset += before.length + + text.insert(offset + to + 1, after) + offset += after.length + + offset end end end diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index 02e097eca3d..a5b041687e3 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -7,6 +7,13 @@ module Gitlab formatter.format(lexer.lex(blob_content, continue: continue)).html_safe end + def self.highlight_lines(repository, ref, file_name) + blob = repository.blob_at(ref, file_name) + return [] unless blob + + highlight(file_name, blob.data).lines.map!(&:html_safe) + end + private def self.rouge_formatter(options = {}) diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index cdeed603e23..3c66c9889ba 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -51,9 +51,9 @@ describe Gitlab::Diff::Highlight, lib: true do end end - describe '.process_file' do + describe '.highlight_lines' do let(:lines) do - Gitlab::Diff::Highlight.process_file(project.repository, commit.id, 'files/ruby/popen.rb') + Gitlab::Diff::Highlight.highlight_lines(project.repository, commit.id, 'files/ruby/popen.rb') end it 'should properly highlight all the lines' do -- cgit v1.2.1 From 70bc322415b33a4789067b81387d30f1411c9091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Thu, 14 Jan 2016 12:56:44 -0500 Subject: Use the adequate reference for the old rev. #3945 --- app/controllers/projects/merge_requests_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 95fd62b8ace..3d792af0a48 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -59,7 +59,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def diffs @commit = @merge_request.last_commit @first_commit = @merge_request.first_commit - @diff_refs = [@merge_request.target_sha, @merge_request.source_sha] + @diff_refs = [@merge_request.last_commit.parent_id, @merge_request.source_sha] @comments_allowed = @reply_allowed = true @comments_target = { -- cgit v1.2.1 From 6b9c730e91962a6d6343bcb7fc4dc75c99b41bde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Thu, 14 Jan 2016 16:38:37 -0500 Subject: More refactoring from last code review. #3945 * Use commit objects instead of IDs when generating diffs * Use proper references when generating MR's source and target * Update broken specs --- app/controllers/projects/commit_controller.rb | 2 +- app/controllers/projects/compare_controller.rb | 2 +- .../projects/merge_requests_controller.rb | 3 - app/helpers/diff_helper.rb | 4 +- app/models/merge_request.rb | 4 + app/views/projects/diffs/_diffs.html.haml | 2 +- .../projects/merge_requests/_new_submit.html.haml | 2 +- .../projects/merge_requests/show/_diffs.html.haml | 3 +- lib/gitlab/diff/file.rb | 5 +- lib/gitlab/diff/highlight.rb | 17 +- spec/fixtures/parallel_diff_result.yml | 590 +++++++++++---------- spec/helpers/diff_helper_spec.rb | 24 +- spec/lib/gitlab/diff/file_spec.rb | 2 +- spec/lib/gitlab/diff/highlight_spec.rb | 2 +- 14 files changed, 366 insertions(+), 296 deletions(-) diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index c8f143dd6b4..ba924ccc5a5 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -72,7 +72,7 @@ class Projects::CommitController < Projects::ApplicationController @diffs = commit.diffs end - @diff_refs = [commit.parent_id || commit.id, commit.id] + @diff_refs = [commit.parent || commit, commit] @notes_count = commit.notes.count @statuses = ci_commit.statuses if ci_commit diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 5b88aed7a64..60360c8e5c8 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -20,9 +20,9 @@ class Projects::CompareController < Projects::ApplicationController if compare_result @commits = Commit.decorate(compare_result.commits, @project) @diffs = compare_result.diffs - @diff_refs = [base_ref, head_ref] @commit = @project.commit(head_ref) @first_commit = @project.commit(base_ref) + @diff_refs = [@first_commit, @commit] @line_notes = [] end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 3d792af0a48..ab5c953189c 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -59,7 +59,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController def diffs @commit = @merge_request.last_commit @first_commit = @merge_request.first_commit - @diff_refs = [@merge_request.last_commit.parent_id, @merge_request.source_sha] @comments_allowed = @reply_allowed = true @comments_target = { @@ -104,8 +103,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commit = @merge_request.last_commit @first_commit = @merge_request.first_commit @diffs = @merge_request.compare_diffs - # We need to use #source_branch because #source_sha requires an existent MergeRequestDiff object. - @diff_refs = [@merge_request.target_sha, @merge_request.source_branch] @ci_commit = @merge_request.ci_commit @statuses = @ci_commit.statuses if @ci_commit diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 1596f9e7d19..425a8ced549 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -19,13 +19,13 @@ module DiffHelper end end - def safe_diff_files(diffs, diff_refs, repository) + def safe_diff_files(diffs, diff_refs) lines = 0 safe_files = [] diffs.first(allowed_diff_size).each do |diff| lines += diff.diff.lines.count break if lines > allowed_diff_lines - safe_files << Gitlab::Diff::File.new(diff, diff_refs, repository) + safe_files << Gitlab::Diff::File.new(diff, diff_refs) end safe_files end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index ac25d38eb63..fe87b820e98 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -511,4 +511,8 @@ class MergeRequest < ActiveRecord::Base def broken? self.commits.blank? || branch_missing? || cannot_be_merged? end + + def diff_range + [last_commit.parent, first_commit] + end end diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 58ad7d79af9..b2f9c14da88 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -1,7 +1,7 @@ - if diff_view == 'parallel' - fluid_layout true -- diff_files = safe_diff_files(diffs, diff_refs, repository) +- diff_files = safe_diff_files(diffs, diff_refs) .gray-content-block.middle-block.oneline-block .inline-parallel-buttons diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 0cb8b6daedb..f2a12099b26 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -38,7 +38,7 @@ = render "projects/merge_requests/show/commits" #diffs.diffs.tab-pane.active - if @diffs.present? - = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @diff_refs + = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @merge_request.diff_range - elsif @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE .alert.alert-danger %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits. diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index 1d9af1e4160..46c6f79937b 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -1,5 +1,6 @@ - if @merge_request_diff.collected? - = render "projects/diffs/diffs", diffs: params[:w] == '1' ? @merge_request.diffs_no_whitespace : @merge_request.diffs, project: @merge_request.project, diff_refs: @diff_refs + = render "projects/diffs/diffs", diffs: params[:w] == '1' ? @merge_request.diffs_no_whitespace : @merge_request.diffs, + project: @merge_request.project, diff_refs: @merge_request.diff_range - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} - else diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index c1a6e16da5a..a6a7fc8ff4c 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -1,14 +1,13 @@ module Gitlab module Diff class File - attr_reader :diff, :repository, :new_ref, :old_ref + attr_reader :diff, :new_ref, :old_ref delegate :new_file, :deleted_file, :renamed_file, :old_path, :new_path, to: :diff, prefix: false - def initialize(diff, diff_refs, repository) + def initialize(diff, diff_refs) @diff = diff - @repository = repository @old_ref, @new_ref = diff_refs end diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index e21f496102d..fb79a2a69de 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -3,8 +3,7 @@ module Gitlab class Highlight attr_reader :diff_file - delegate :repository, :old_path, :new_path, :old_ref, :new_ref, - to: :diff_file, prefix: :diff + delegate :old_path, :new_path, :old_ref, :new_ref, to: :diff_file, prefix: :diff def initialize(diff_file) @diff_file = diff_file @@ -141,11 +140,11 @@ module Gitlab end def old_lines - @old_lines ||= Gitlab::Highlight.highlight_lines(diff_repository, diff_old_ref, diff_old_path) + @old_lines ||= self.class.process_file(*processing_args(:old)) end def new_lines - @new_lines ||= Gitlab::Highlight.highlight_lines(diff_repository, diff_new_ref, diff_new_path) + @new_lines ||= self.class.process_file(*processing_args(:new)) end def longest_common_suffix(a, b) @@ -200,6 +199,16 @@ module Gitlab offset end + + private + + def processing_args(version) + ref = send("diff_#{version}_ref") + path = send("diff_#{version}_path") + + [ref.project.repository, ref.id, path] + end + end end end diff --git a/spec/fixtures/parallel_diff_result.yml b/spec/fixtures/parallel_diff_result.yml index 020c1817226..4583de1231e 100644 --- a/spec/fixtures/parallel_diff_result.yml +++ b/spec/fixtures/parallel_diff_result.yml @@ -1,268 +1,324 @@ --- -- - match - - 6 - - "@@ -6,12 +6,18 @@ module Popen" - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 - - match - - 6 - - "@@ -6,12 +6,18 @@ module Popen" - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 -- - - - 6 - - |2 - - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 - - - - 6 - - |2 - - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 -- - - - 7 - - |2 - def popen(cmd, path=nil) - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7 - - - - 7 - - |2 - def popen(cmd, path=nil) - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7 -- - - - 8 - - |2 - unless cmd.is_a?(Array) - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8 - - - - 8 - - |2 - unless cmd.is_a?(Array) - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8 -- - old - - 9 - - | - - raise "System commands must be given as an array of strings" - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9 - - new - - 9 - - | - + raise RuntimeError, "System commands must be given as an array of strings" - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9 -- - - - 10 - - |2 - end - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10 - - - - 10 - - |2 - end - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10 -- - - - 11 - - |2 - - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11 - - - - 11 - - |2 - - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11 -- - - - 12 - - |2 - path ||= Dir.pwd - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12 - - - - 12 - - |2 - path ||= Dir.pwd - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12 -- - old - - 13 - - | - - vars = { "PWD" => path } - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_13_13 - - old - - - - " " - - -- - old - - 14 - - | - - options = { chdir: path } - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_13 - - new - - 13 - - | - + - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_13 -- - - - - - " " - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14 - - new - - 14 - - | - + vars = { - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14 -- - - - - - " " - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15 - - new - - 15 - - | - + "PWD" => path - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15 -- - - - - - " " - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16 - - new - - 16 - - | - + } - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16 -- - - - - - " " - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17 - - new - - 17 - - | - + - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17 -- - - - - - " " - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18 - - new - - 18 - - | - + options = { - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18 -- - - - - - " " - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19 - - new - - 19 - - | - + chdir: path - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19 -- - - - - - " " - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20 - - new - - 20 - - | - + } - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20 -- - - - 15 - - |2 - - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21 - - - - 21 - - |2 - - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21 -- - - - 16 - - |2 - unless File.directory?(path) - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22 - - - - 22 - - |2 - unless File.directory?(path) - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22 -- - - - 17 - - |2 - FileUtils.mkdir_p(path) - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23 - - - - 23 - - |2 - FileUtils.mkdir_p(path) - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23 -- - match - - 19 - - "@@ -19,6 +25,7 @@ module Popen" - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 - - match - - 25 - - "@@ -19,6 +25,7 @@ module Popen" - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 -- - - - 19 - - |2 - - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 - - - - 25 - - |2 - - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 -- - - - 20 - - |2 - @cmd_output = "" - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26 - - - - 26 - - |2 - @cmd_output = "" - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26 -- - - - 21 - - |2 - @cmd_status = 0 - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27 - - - - 27 - - |2 - @cmd_status = 0 - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27 -- - - - - - " " - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28 - - new - - 28 - - | - + - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28 -- - - - 22 - - |2 - Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29 - - - - 29 - - |2 - Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29 -- - - - 23 - - |2 - @cmd_output << stdout.read - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30 - - - - 30 - - |2 - @cmd_output << stdout.read - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30 -- - - - 24 - - |2 - @cmd_output << stderr.read - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31 - - - - 31 - - |2 - @cmd_output << stderr.read - - 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31 +- :left: + :type: match + :number: 6 + :text: "@@ -6,12 +6,18 @@ module Popen" + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 + :right: + :type: match + :number: 6 + :text: "@@ -6,12 +6,18 @@ module Popen" + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 +- :left: + :type: + :number: 6 + :text: |2 + + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 + :right: + :type: + :number: 6 + :text: |2 + + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 +- :left: + :type: + :number: 7 + :text: |2 + def popen(cmd, path=nil) + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7 + :right: + :type: + :number: 7 + :text: |2 + def popen(cmd, path=nil) + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7 +- :left: + :type: + :number: 8 + :text: |2 + unless cmd.is_a?(Array) + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8 + :right: + :type: + :number: 8 + :text: |2 + unless cmd.is_a?(Array) + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8 +- :left: + :type: old + :number: 9 + :text: | + - raise "System commands must be given as an array of strings" + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9 + :right: + :type: new + :number: 9 + :text: | + + raise RuntimeError, "System commands must be given as an array of strings" + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9 +- :left: + :type: + :number: 10 + :text: |2 + end + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10 + :right: + :type: + :number: 10 + :text: |2 + end + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10 +- :left: + :type: + :number: 11 + :text: |2 + + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11 + :right: + :type: + :number: 11 + :text: |2 + + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11 +- :left: + :type: + :number: 12 + :text: |2 + path ||= Dir.pwd + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12 + :right: + :type: + :number: 12 + :text: |2 + path ||= Dir.pwd + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12 +- :left: + :type: old + :number: 13 + :text: | + - vars = { "PWD" => path } + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_13_13 + :right: + :type: old + :number: + :text: '' + :line_code: +- :left: + :type: old + :number: 14 + :text: | + - options = { chdir: path } + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_13 + :right: + :type: new + :number: 13 + :text: | + + + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_13 +- :left: + :type: + :number: + :text: '' + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14 + :right: + :type: new + :number: 14 + :text: | + + vars = { + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14 +- :left: + :type: + :number: + :text: '' + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15 + :right: + :type: new + :number: 15 + :text: | + + "PWD" => path + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15 +- :left: + :type: + :number: + :text: '' + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16 + :right: + :type: new + :number: 16 + :text: | + + } + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16 +- :left: + :type: + :number: + :text: '' + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17 + :right: + :type: new + :number: 17 + :text: | + + + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17 +- :left: + :type: + :number: + :text: '' + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18 + :right: + :type: new + :number: 18 + :text: | + + options = { + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18 +- :left: + :type: + :number: + :text: '' + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19 + :right: + :type: new + :number: 19 + :text: | + + chdir: path + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19 +- :left: + :type: + :number: + :text: '' + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20 + :right: + :type: new + :number: 20 + :text: | + + } + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20 +- :left: + :type: + :number: 15 + :text: |2 + + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21 + :right: + :type: + :number: 21 + :text: |2 + + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21 +- :left: + :type: + :number: 16 + :text: |2 + unless File.directory?(path) + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22 + :right: + :type: + :number: 22 + :text: |2 + unless File.directory?(path) + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22 +- :left: + :type: + :number: 17 + :text: |2 + FileUtils.mkdir_p(path) + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23 + :right: + :type: + :number: 23 + :text: |2 + FileUtils.mkdir_p(path) + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23 +- :left: + :type: match + :number: 19 + :text: "@@ -19,6 +25,7 @@ module Popen" + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 + :right: + :type: match + :number: 25 + :text: "@@ -19,6 +25,7 @@ module Popen" + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 +- :left: + :type: + :number: 19 + :text: |2 + + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 + :right: + :type: + :number: 25 + :text: |2 + + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 +- :left: + :type: + :number: 20 + :text: |2 + @cmd_output = "" + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26 + :right: + :type: + :number: 26 + :text: |2 + @cmd_output = "" + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26 +- :left: + :type: + :number: 21 + :text: |2 + @cmd_status = 0 + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27 + :right: + :type: + :number: 27 + :text: |2 + @cmd_status = 0 + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27 +- :left: + :type: + :number: + :text: '' + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28 + :right: + :type: new + :number: 28 + :text: | + + + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28 +- :left: + :type: + :number: 22 + :text: |2 + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29 + :right: + :type: + :number: 29 + :text: |2 + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29 +- :left: + :type: + :number: 23 + :text: |2 + @cmd_output << stdout.read + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30 + :right: + :type: + :number: 30 + :text: |2 + @cmd_output << stdout.read + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30 +- :left: + :type: + :number: 24 + :text: |2 + @cmd_output << stderr.read + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31 + :right: + :type: + :number: 31 + :text: |2 + @cmd_output << stderr.read + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31 diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 4b10ee3d33c..435ecd04fbe 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -8,8 +8,8 @@ describe DiffHelper do let(:commit) { project.commit(sample_commit.id) } let(:diffs) { commit.diffs } let(:diff) { diffs.first } - let(:diff_refs) { [commit.parent.id, commit.id] } - let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs, repository) } + let(:diff_refs) { [commit.parent, commit] } + let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs) } describe 'diff_hard_limit_enabled?' do it 'should return true if param is provided' do @@ -46,41 +46,41 @@ describe DiffHelper do describe 'safe_diff_files' do it 'should return all files from a commit that is smaller than safe limits' do - expect(safe_diff_files(diffs, diff_refs, repository).length).to eq(2) + expect(safe_diff_files(diffs, diff_refs).length).to eq(2) end it 'should return only the first file if the diff line count in the 2nd file takes the total beyond safe limits' do allow(diffs[1].diff).to receive(:lines).and_return([""] * 4999) #simulate 4999 lines - expect(safe_diff_files(diffs, diff_refs, repository).length).to eq(1) + expect(safe_diff_files(diffs, diff_refs).length).to eq(1) end it 'should return all files from a commit that is beyond safe limit for numbers of lines if force diff is true' do allow(controller).to receive(:params) { { force_show_diff: true } } allow(diffs[1].diff).to receive(:lines).and_return([""] * 4999) #simulate 4999 lines - expect(safe_diff_files(diffs, diff_refs, repository).length).to eq(2) + expect(safe_diff_files(diffs, diff_refs).length).to eq(2) end it 'should return only the first file if the diff line count in the 2nd file takes the total beyond hard limits' do allow(controller).to receive(:params) { { force_show_diff: true } } allow(diffs[1].diff).to receive(:lines).and_return([""] * 49999) #simulate 49999 lines - expect(safe_diff_files(diffs, diff_refs, repository).length).to eq(1) + expect(safe_diff_files(diffs, diff_refs).length).to eq(1) end it 'should return only a safe number of file diffs if a commit touches more files than the safe limits' do large_diffs = diffs * 100 #simulate 200 diffs - expect(safe_diff_files(large_diffs, diff_refs, repository).length).to eq(100) + expect(safe_diff_files(large_diffs, diff_refs).length).to eq(100) end it 'should return all file diffs if a commit touches more files than the safe limits but force diff is true' do allow(controller).to receive(:params) { { force_show_diff: true } } large_diffs = diffs * 100 #simulate 200 diffs - expect(safe_diff_files(large_diffs, diff_refs, repository).length).to eq(200) + expect(safe_diff_files(large_diffs, diff_refs).length).to eq(200) end it 'should return a limited file diffs if a commit touches more files than the hard limits and force diff is true' do allow(controller).to receive(:params) { { force_show_diff: true } } very_large_diffs = diffs * 1000 #simulate 2000 diffs - expect(safe_diff_files(very_large_diffs, diff_refs, repository).length).to eq(1000) + expect(safe_diff_files(very_large_diffs, diff_refs).length).to eq(1000) end end @@ -128,7 +128,11 @@ describe DiffHelper do expect(diff_line_content(diff_file.diff_lines.first.text)). to eq('@@ -6,12 +6,18 @@ module Popen') expect(diff_line_content(diff_file.diff_lines.first.type)).to eq('match') - expect(diff_line_content(diff_file.diff_lines.first.new_pos)).to eq(6) + expect(diff_file.diff_lines.first.new_pos).to eq(6) + end + + it 'should return safe HTML' do + expect(diff_line_content(diff_file.diff_lines.first.text)).to be_html_safe end end diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index 97ba7f14eb1..0d9694f2c13 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Diff::File, lib: true do let(:project) { create(:project) } let(:commit) { project.commit(sample_commit.id) } let(:diff) { commit.diffs.first } - let(:diff_file) { Gitlab::Diff::File.new(diff, [commit.parent.id, commit.id], project.repository) } + let(:diff_file) { Gitlab::Diff::File.new(diff, [commit.parent, commit]) } describe :diff_lines do let(:diff_lines) { diff_file.diff_lines } diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index 3c66c9889ba..f307dcaae44 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Diff::Highlight, lib: true do let(:project) { create(:project) } let(:commit) { project.commit(sample_commit.id) } let(:diff) { commit.diffs.first } - let(:diff_file) { Gitlab::Diff::File.new(diff, [commit.parent.id, commit.id], project.repository) } + let(:diff_file) { Gitlab::Diff::File.new(diff, [commit.parent, commit]) } describe '.process_diff_lines' do context 'when processing Gitlab::Diff::Line objects' do -- cgit v1.2.1 From 71b4341a37dc274ce94d0b967e5b69fd49834cb8 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 15 Jan 2016 14:11:36 +0100 Subject: Method was moved --- lib/gitlab/diff/highlight.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index fb79a2a69de..65deea9d335 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -140,11 +140,11 @@ module Gitlab end def old_lines - @old_lines ||= self.class.process_file(*processing_args(:old)) + @old_lines ||= Gitlab::Highlight.highlight_lines(*processing_args(:old)) end def new_lines - @new_lines ||= self.class.process_file(*processing_args(:new)) + @new_lines ||= Gitlab::Highlight.highlight_lines(*processing_args(:new)) end def longest_common_suffix(a, b) -- cgit v1.2.1 From 7d31f372191c38e5676cd6ec746721eb64b4a634 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 15 Jan 2016 14:25:29 +0100 Subject: Move inline diff logic to its own class --- lib/gitlab/diff/highlight.rb | 84 ++++++-------------------------------------- 1 file changed, 11 insertions(+), 73 deletions(-) diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index 65deea9d335..f89121e184b 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -24,26 +24,7 @@ module Gitlab private def find_inline_diffs - @inline_diffs = [] - - local_edit_indexes.each do |index| - old_index = index - new_index = index + 1 - old_line = @raw_lines[old_index][1..-1] - new_line = @raw_lines[new_index][1..-1] - - # Skip inline diff if empty line was replaced with content - next if old_line == "" - - lcp = longest_common_prefix(old_line, new_line) - lcs = longest_common_suffix(old_line, new_line) - - old_diff_range = lcp..(old_line.length - lcs - 1) - new_diff_range = lcp..(new_line.length - lcs - 1) - - @inline_diffs[old_index] = old_diff_range if old_diff_range.begin <= old_diff_range.end - @inline_diffs[new_index] = new_diff_range if new_diff_range.begin <= new_diff_range.end - end + @inline_diffs = InlineDiff.new(@raw_lines).inline_diffs end def process_lines @@ -58,7 +39,7 @@ module Gitlab end def highlight_line(diff_line, index) - line_prefix = line_prefixes[index] + line_prefix = diff_line.text.match(/\A([+-])/) ? $1 : ' ' case diff_line.type when 'new', nil @@ -73,44 +54,23 @@ module Gitlab end def mark_inline_diffs(rich_line, diff_line, index) - inline_diff = @inline_diffs[index] - return rich_line unless inline_diff + line_inline_diffs = @inline_diffs[index] + return rich_line unless line_inline_diffs raw_line = diff_line.text - - # Based on the prefixless versions - from = inline_diff.begin + 1 - to = inline_diff.end + 1 - position_mapping = map_character_positions(raw_line, rich_line) - inline_diff_positions = position_mapping[from..to] - marker_ranges = collapse_ranges(inline_diff_positions) offset = 0 - marker_ranges.each do |range| - offset = insert_around_range(rich_line, range, "", "", offset) - end - - rich_line - end - - def line_prefixes - @line_prefixes ||= @raw_lines.map { |line| line.match(/\A([+-])/) ? $1 : ' ' } - end - - def local_edit_indexes - @local_edit_indexes ||= begin - joined_line_prefixes = " #{line_prefixes.join} " + line_inline_diffs.each do |inline_diff_range| + inline_diff_positions = position_mapping[inline_diff_range] + marker_ranges = collapse_ranges(inline_diff_positions) - offset = 0 - local_edit_indexes = [] - while index = joined_line_prefixes.index(" -+ ", offset) - local_edit_indexes << index - offset = index + 1 + marker_ranges.each do |range| + offset = insert_around_range(rich_line, range, "", "", offset) end - - local_edit_indexes end + + rich_line end def map_character_positions(raw_line, rich_line) @@ -147,25 +107,6 @@ module Gitlab @new_lines ||= Gitlab::Highlight.highlight_lines(*processing_args(:new)) end - def longest_common_suffix(a, b) - longest_common_prefix(a.reverse, b.reverse) - end - - def longest_common_prefix(a, b) - max_length = [a.length, b.length].max - - length = 0 - (0..max_length - 1).each do |pos| - old_char = a[pos] - new_char = b[pos] - - break if old_char != new_char - length += 1 - end - - length - end - def collapse_ranges(positions) return [] if positions.empty? ranges = [] @@ -200,15 +141,12 @@ module Gitlab offset end - private - def processing_args(version) ref = send("diff_#{version}_ref") path = send("diff_#{version}_path") [ref.project.repository, ref.id, path] end - end end end -- cgit v1.2.1 From 13f10efcf120f2d5244bf6abe934dc7c026834ef Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 15 Jan 2016 14:39:43 +0100 Subject: Move inline diff marker logic to its own class --- lib/gitlab/diff/highlight.rb | 106 +++++-------------------------------------- 1 file changed, 11 insertions(+), 95 deletions(-) diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index f89121e184b..b6875f07279 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -12,32 +12,24 @@ module Gitlab end def highlight - return [] if @diff_lines.empty? - - find_inline_diffs - - process_lines - - @diff_lines - end - - private - - def find_inline_diffs - @inline_diffs = InlineDiff.new(@raw_lines).inline_diffs - end - - def process_lines @diff_lines.each_with_index do |diff_line, i| # ignore highlighting for "match" lines next if diff_line.type == 'match' rich_line = highlight_line(diff_line, i) - rich_line = mark_inline_diffs(rich_line, diff_line, i) + + if line_inline_diffs = inline_diffs[i] + rich_line = InlineDiffMarker.new(diff_line.text, rich_line).mark(line_inline_diffs) + end + diff_line.text = rich_line.html_safe end + + @diff_lines end + private + def highlight_line(diff_line, index) line_prefix = diff_line.text.match(/\A([+-])/) ? $1 : ' ' @@ -53,50 +45,8 @@ module Gitlab rich_line ? line_prefix + rich_line : diff_line.text end - def mark_inline_diffs(rich_line, diff_line, index) - line_inline_diffs = @inline_diffs[index] - return rich_line unless line_inline_diffs - - raw_line = diff_line.text - position_mapping = map_character_positions(raw_line, rich_line) - - offset = 0 - line_inline_diffs.each do |inline_diff_range| - inline_diff_positions = position_mapping[inline_diff_range] - marker_ranges = collapse_ranges(inline_diff_positions) - - marker_ranges.each do |range| - offset = insert_around_range(rich_line, range, "", "", offset) - end - end - - rich_line - end - - def map_character_positions(raw_line, rich_line) - mapping = [] - raw_pos = 0 - rich_pos = 0 - (0..raw_line.length).each do |raw_pos| - raw_char = raw_line[raw_pos] - rich_char = rich_line[rich_pos] - - while rich_char == '<' - until rich_char == '>' - rich_pos += 1 - rich_char = rich_line[rich_pos] - end - - rich_pos += 1 - rich_char = rich_line[rich_pos] - end - - mapping[raw_pos] = rich_pos - - rich_pos += 1 - end - - mapping + def inline_diffs + @inline_diffs ||= InlineDiff.new(@raw_lines).inline_diffs end def old_lines @@ -107,40 +57,6 @@ module Gitlab @new_lines ||= Gitlab::Highlight.highlight_lines(*processing_args(:new)) end - def collapse_ranges(positions) - return [] if positions.empty? - ranges = [] - - start = prev = positions[0] - range = start..prev - positions[1..-1].each do |pos| - if pos == prev + 1 - range = start..pos - prev = pos - else - ranges << range - start = prev = pos - range = start..prev - end - end - ranges << range - - ranges - end - - def insert_around_range(text, range, before, after, offset = 0) - from = range.begin - to = range.end - - text.insert(offset + from, before) - offset += before.length - - text.insert(offset + to + 1, after) - offset += after.length - - offset - end - def processing_args(version) ref = send("diff_#{version}_ref") path = send("diff_#{version}_path") -- cgit v1.2.1 From 5de8971ddfa10cbc6a082316eee7377038670f75 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 15 Jan 2016 16:02:48 +0100 Subject: Whoops, forgot to add files --- lib/gitlab/diff/inline_diff.rb | 75 +++++++++++++++++++++++++++++++ lib/gitlab/diff/inline_diff_marker.rb | 85 +++++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 lib/gitlab/diff/inline_diff.rb create mode 100644 lib/gitlab/diff/inline_diff_marker.rb diff --git a/lib/gitlab/diff/inline_diff.rb b/lib/gitlab/diff/inline_diff.rb new file mode 100644 index 00000000000..bae297ab00f --- /dev/null +++ b/lib/gitlab/diff/inline_diff.rb @@ -0,0 +1,75 @@ +module Gitlab + module Diff + class InlineDiff + attr_accessor :lines + + def initialize(lines) + @lines = lines + end + + def inline_diffs + inline_diffs = [] + + local_edit_indexes.each do |index| + old_index = index + new_index = index + 1 + old_line = @lines[old_index] + new_line = @lines[new_index] + + suffixless_old_line = old_line[1..-1] + suffixless_new_line = new_line[1..-1] + + # Skip inline diff if empty line was replaced with content + next if suffixless_old_line == "" + + # Add one, because this is based on the suffixless version + lcp = longest_common_prefix(suffixless_old_line, suffixless_new_line) + 1 + lcs = longest_common_suffix(suffixless_old_line, suffixless_new_line) + + old_diff_range = lcp..(old_line.length - lcs - 1) + new_diff_range = lcp..(new_line.length - lcs - 1) + + inline_diffs[old_index] = [old_diff_range] if old_diff_range.begin <= old_diff_range.end + inline_diffs[new_index] = [new_diff_range] if new_diff_range.begin <= new_diff_range.end + end + + inline_diffs + end + + private + + def local_edit_indexes + line_prefixes = @lines.map { |line| line.match(/\A([+-])/) ? $1 : ' ' } + joined_line_prefixes = " #{line_prefixes.join} " + + offset = 0 + local_edit_indexes = [] + while index = joined_line_prefixes.index(" -+ ", offset) + local_edit_indexes << index + offset = index + 1 + end + + local_edit_indexes + end + + def longest_common_prefix(a, b) + max_length = [a.length, b.length].max + + length = 0 + (0..max_length - 1).each do |pos| + old_char = a[pos] + new_char = b[pos] + + break if old_char != new_char + length += 1 + end + + length + end + + def longest_common_suffix(a, b) + longest_common_prefix(a.reverse, b.reverse) + end + end + end +end diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb new file mode 100644 index 00000000000..4bb755e3d3d --- /dev/null +++ b/lib/gitlab/diff/inline_diff_marker.rb @@ -0,0 +1,85 @@ +module Gitlab + module Diff + class InlineDiffMarker + attr_accessor :raw_line, :rich_line + + def initialize(raw_line, rich_line = raw_line) + @raw_line = raw_line + @rich_line = rich_line + end + + def mark(line_inline_diffs) + offset = 0 + line_inline_diffs.each do |inline_diff_range| + inline_diff_positions = position_mapping[inline_diff_range] + marker_ranges = collapse_ranges(inline_diff_positions) + + marker_ranges.each do |range| + offset = insert_around_range(rich_line, range, "", "", offset) + end + end + + rich_line + end + + def position_mapping + @position_mapping ||= begin + mapping = [] + raw_pos = 0 + rich_pos = 0 + (0..raw_line.length).each do |raw_pos| + raw_char = raw_line[raw_pos] + rich_char = rich_line[rich_pos] + + while rich_char == '<' + until rich_char == '>' + rich_pos += 1 + rich_char = rich_line[rich_pos] + end + + rich_pos += 1 + rich_char = rich_line[rich_pos] + end + + mapping[raw_pos] = rich_pos + + rich_pos += 1 + end + + mapping + end + end + + def collapse_ranges(positions) + return [] if positions.empty? + ranges = [] + + start = prev = positions[0] + range = start..prev + positions[1..-1].each do |pos| + if pos == prev + 1 + range = start..pos + prev = pos + else + ranges << range + start = prev = pos + range = start..prev + end + end + ranges << range + + ranges + end + + def insert_around_range(text, range, before, after, offset = 0) + text.insert(offset + range.begin, before) + offset += before.length + + text.insert(offset + range.end + 1, after) + offset += after.length + + offset + end + end + end +end -- cgit v1.2.1 From 12546f895f86929ffe69299a02c93692370ee55a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 15 Jan 2016 16:29:26 +0100 Subject: Add tests --- lib/gitlab/diff/inline_diff.rb | 1 + lib/gitlab/diff/inline_diff_marker.rb | 10 +++++++++ spec/lib/gitlab/diff/inline_diff_marker_spec.rb | 15 ++++++++++++++ spec/lib/gitlab/diff/inline_diff_spec.rb | 27 +++++++++++++++++++++++++ 4 files changed, 53 insertions(+) create mode 100644 spec/lib/gitlab/diff/inline_diff_marker_spec.rb create mode 100644 spec/lib/gitlab/diff/inline_diff_spec.rb diff --git a/lib/gitlab/diff/inline_diff.rb b/lib/gitlab/diff/inline_diff.rb index bae297ab00f..e5986fd69e2 100644 --- a/lib/gitlab/diff/inline_diff.rb +++ b/lib/gitlab/diff/inline_diff.rb @@ -38,6 +38,7 @@ module Gitlab private + # Find runs of single line edits def local_edit_indexes line_prefixes = @lines.map { |line| line.match(/\A([+-])/) ? $1 : ' ' } joined_line_prefixes = " #{line_prefixes.join} " diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb index 4bb755e3d3d..405465a641f 100644 --- a/lib/gitlab/diff/inline_diff_marker.rb +++ b/lib/gitlab/diff/inline_diff_marker.rb @@ -11,9 +11,12 @@ module Gitlab def mark(line_inline_diffs) offset = 0 line_inline_diffs.each do |inline_diff_range| + # Map the inline-diff range based on the raw line to character positions in the rich line inline_diff_positions = position_mapping[inline_diff_range] + # Turn the array of character positions into ranges marker_ranges = collapse_ranges(inline_diff_positions) + # Mark each range marker_ranges.each do |range| offset = insert_around_range(rich_line, range, "", "", offset) end @@ -22,6 +25,9 @@ module Gitlab rich_line end + private + + # Mapping of character positions in the raw line, to the rich (highlighted) line def position_mapping @position_mapping ||= begin mapping = [] @@ -31,6 +37,8 @@ module Gitlab raw_char = raw_line[raw_pos] rich_char = rich_line[rich_pos] + # The raw and rich lines are the same except for HTML tags, + # so skip over any `<...>` segment while rich_char == '<' until rich_char == '>' rich_pos += 1 @@ -50,6 +58,7 @@ module Gitlab end end + # Takes an array of integers, and returns an array of ranges covering the same integers def collapse_ranges(positions) return [] if positions.empty? ranges = [] @@ -71,6 +80,7 @@ module Gitlab ranges end + # Inserts tags around the characters identified by the given range def insert_around_range(text, range, before, after, offset = 0) text.insert(offset + range.begin, before) offset += before.length diff --git a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb new file mode 100644 index 00000000000..5d6aea17509 --- /dev/null +++ b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe Gitlab::Diff::InlineDiffMarker, lib: true do + describe '#inline_diffs' do + let(:raw) { "abc def" } + let(:rich) { %{abc def} } + let(:inline_diffs) { [2..4] } + + let(:subject) { Gitlab::Diff::InlineDiffMarker.new(raw, rich).mark(inline_diffs) } + + it 'marks the inline diffs' do + expect(subject).to eq(%{abc def}) + end + end +end diff --git a/spec/lib/gitlab/diff/inline_diff_spec.rb b/spec/lib/gitlab/diff/inline_diff_spec.rb new file mode 100644 index 00000000000..056917df893 --- /dev/null +++ b/spec/lib/gitlab/diff/inline_diff_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Gitlab::Diff::InlineDiff, lib: true do + describe '#inline_diffs' do + let(:diff) do + < Date: Mon, 18 Jan 2016 08:21:01 +0100 Subject: Refactor branches API documentation [ci skip] --- doc/api/branches.md | 108 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 73 insertions(+), 35 deletions(-) diff --git a/doc/api/branches.md b/doc/api/branches.md index 6a9c10c8520..abc4732c395 100644 --- a/doc/api/branches.md +++ b/doc/api/branches.md @@ -8,13 +8,21 @@ Get a list of repository branches from a project, sorted by name alphabetically. GET /projects/:id/repository/branches ``` -Parameters: +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | -- `id` (required) - The ID of a project +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches +``` + +Example response: ```json [ { + "name": "master", + "protected": true, "commit": { "author_email": "john@example.com", "author_name": "John Smith", @@ -27,10 +35,9 @@ Parameters: "parent_ids": [ "4ad91d3c1144c406e50c7b33bae684bd6837faf8" ] - }, - "name": "master", - "protected": true - } + } + }, + ... ] ``` @@ -42,13 +49,21 @@ Get a single project repository branch. GET /projects/:id/repository/branches/:branch ``` -Parameters: +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `branch` | string | yes | The name of the branch | -- `id` (required) - The ID of a project -- `branch` (required) - The name of the branch +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master +``` + +Example response: ```json { + "name": "master", + "protected": true, "commit": { "author_email": "john@example.com", "author_name": "John Smith", @@ -61,25 +76,30 @@ Parameters: "parent_ids": [ "4ad91d3c1144c406e50c7b33bae684bd6837faf8" ] - }, - "name": "master", - "protected": true + } } ``` ## Protect repository branch -Protects a single project repository branch. This is an idempotent function, protecting an already -protected repository branch still returns a `200 OK` status code. +Protects a single project repository branch. This is an idempotent function, +protecting an already protected repository branch still returns a `200 OK` +status code. ``` PUT /projects/:id/repository/branches/:branch/protect ``` -Parameters: +```bash +curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master/protect +``` -- `id` (required) - The ID of a project -- `branch` (required) - The name of the branch +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `branch` | string | yes | The name of the branch | + +Example response: ```json { @@ -103,17 +123,24 @@ Parameters: ## Unprotect repository branch -Unprotects a single project repository branch. This is an idempotent function, unprotecting an already -unprotected repository branch still returns a `200 OK` status code. +Unprotects a single project repository branch. This is an idempotent function, +unprotecting an already unprotected repository branch still returns a `200 OK` +status code. ``` PUT /projects/:id/repository/branches/:branch/unprotect ``` -Parameters: +```bash +curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master/unprotect +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `branch` | string | yes | The name of the branch | -- `id` (required) - The ID of a project -- `branch` (required) - The name of the branch +Example response: ```json { @@ -141,11 +168,17 @@ Parameters: POST /projects/:id/repository/branches ``` -Parameters: +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `branch_name` | string | yes | The name of the branch | +| `ref` | string | yes | The branch name or commit SHA to create branch from | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches?branch_name=newbranch&ref=master" +``` -- `id` (required) - The ID of a project -- `branch_name` (required) - The name of the branch -- `ref` (required) - Create branch from commit SHA or existing branch +Example response: ```json { @@ -162,12 +195,13 @@ Parameters: "4ad91d3c1144c406e50c7b33bae684bd6837faf8" ] }, - "name": "master", + "name": "newbranch", "protected": false } ``` -It return 200 if succeed or 400 if failed with error message explaining reason. +It returns `200` if it succeeds or `400` if failed with an error message +explaining the reason. ## Delete repository branch @@ -175,18 +209,22 @@ It return 200 if succeed or 400 if failed with error message explaining reason. DELETE /projects/:id/repository/branches/:branch ``` -Parameters: +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `branch` | string | yes | The name of the branch | -- `id` (required) - The ID of a project -- `branch` (required) - The name of the branch +It returns `200` if it succeeds, `404` if the branch to be deleted does not exist +or `400` for other reasons. In case of an error, an explaining message is provided. -It return 200 if succeed, 404 if the branch to be deleted does not exist -or 400 for other reasons. In case of an error, an explaining message is provided. +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches/newbranch" +``` -Success response: +Example response: ```json { - "branch_name": "my-removed-branch" + "branch_name": "newbranch" } ``` -- cgit v1.2.1 From c870bb796bbbc424fdd369c8237b08b706215d03 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 18 Jan 2016 08:25:38 +0100 Subject: Refactor commits API documentation [ci skip] --- doc/api/commits.md | 74 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 17 deletions(-) diff --git a/doc/api/commits.md b/doc/api/commits.md index 93d62b751e6..a07c9dff54e 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -8,10 +8,16 @@ Get a list of repository commits in a project. GET /projects/:id/repository/commits ``` -Parameters: +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `ref_name` | string | no | The name of a repository branch or tag or if not given the default branch | -- `id` (required) - The ID of a project -- `ref_name` (optional) - The name of a repository branch or tag or if not given the default branch +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits" +``` + +Example response: ```json [ @@ -48,8 +54,16 @@ GET /projects/:id/repository/commits/:sha Parameters: -- `id` (required) - The ID of a project -- `sha` (required) - The commit hash or name of a repository branch or tag +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `sha` | string | yes | The commit hash or name of a repository branch or tag | + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master +``` + +Example response: ```json { @@ -79,8 +93,16 @@ GET /projects/:id/repository/commits/:sha/diff Parameters: -- `id` (required) - The ID of a project -- `sha` (required) - The name of a repository branch or tag or if not given the default branch +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `sha` | string | yes | The commit hash or name of a repository branch or tag | + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/diff" +``` + +Example response: ```json [ @@ -107,8 +129,16 @@ GET /projects/:id/repository/commits/:sha/comments Parameters: -- `id` (required) - The ID of a project -- `sha` (required) - The name of a repository branch or tag or if not given the default branch +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `sha` | string | yes | The commit hash or name of a repository branch or tag | + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/comments" +``` + +Example response: ```json [ @@ -128,20 +158,30 @@ Parameters: ## Post comment to commit -Adds a comment to a commit. Optionally you can post comments on a specific line of a commit. Therefor both `path`, `line_new` and `line_old` are required. +Adds a comment to a commit. Optionally you can post comments on a specific line +of a commit. In that case `path`, `line` and `line_type` are required. ``` POST /projects/:id/repository/commits/:sha/comments ``` -Parameters: +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `sha` | string | yes | The commit hash or name of a repository branch or tag | +| `note` | string | yes | Text of comment | +| `path` | string | no | The file path relative to the repository | +| `line` | integer | no | The line number | +| `line_type` | string | no | The line type. Takes `new` or `old` as arguments | + +In order to post a comment in a particular line of a particular file, you must +specify `path`, `line` and `line_type` should be `new`. + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/comments?note=New%20comment&path=CHANGELOG&line=664&line_type=new" +``` -- `id` (required) - The ID of a project -- `sha` (required) - The name of a repository branch or tag or if not given the default branch -- `note` (required) - Text of comment -- `path` (optional) - The file path -- `line` (optional) - The line number -- `line_type` (optional) - The line type (new or old) +Example response: ```json { -- cgit v1.2.1 From 13f52de0fa0585df0da0d0579899be86e4bfe1c9 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 18 Jan 2016 09:10:46 +0100 Subject: Refactor issues API documentation [ci skip] --- doc/api/issues.md | 404 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 260 insertions(+), 144 deletions(-) diff --git a/doc/api/issues.md b/doc/api/issues.md index d407bc35d79..9e704648b25 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -1,9 +1,20 @@ # Issues +Every API call to issues must be authenticated. + +If a user is not a member of a project and the project is private, a `GET` +request on that project will result to a `404` status code. + +## Issues pagination + +By default, `GET` requests return 20 results at a time because the API results +are paginated. + +Read more on [pagination](README.md#pagination). + ## List issues -Get all issues created by authenticated user. This function takes pagination parameters -`page` and `per_page` to restrict the list of issues. +Get all issues created by the authenticated user. ``` GET /issues @@ -14,81 +25,65 @@ GET /issues?labels=foo,bar GET /issues?labels=foo,bar&state=opened ``` -Parameters: +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `state` | string | no | Return all issues or just those that are `opened` or `closed`| +| `labels` | string | no | Comma-separated list of label names | +| `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | +| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/issues +``` -- `state` (optional) - Return `all` issues or just those that are `opened` or `closed` -- `labels` (optional) - Comma-separated list of label names -- `order_by` (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` -- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` +Example response: ```json [ - { - "id": 43, - "iid": 3, - "project_id": 8, - "title": "4xx/5xx pages", - "description": "", - "labels": [], - "milestone": null, - "assignee": null, - "author": { - "id": 1, - "username": "john_smith", - "email": "john@example.com", - "name": "John Smith", - "state": "active", - "created_at": "2012-05-23T08:00:58Z" - }, - "state": "closed", - "updated_at": "2012-07-02T17:53:12Z", - "created_at": "2012-07-02T17:53:12Z" - }, - { - "id": 42, - "iid": 4, - "project_id": 8, - "title": "Add user settings", - "description": "", - "labels": [ - "feature" - ], - "milestone": { - "id": 1, - "title": "v1.0", - "description": "", - "due_date": "2012-07-20", - "state": "reopened", - "updated_at": "2012-07-04T13:42:48Z", - "created_at": "2012-07-04T13:42:48Z" - }, - "assignee": { - "id": 2, - "username": "jack_smith", - "email": "jack@example.com", - "name": "Jack Smith", - "state": "active", - "created_at": "2012-05-23T08:01:01Z" - }, - "author": { - "id": 1, - "username": "john_smith", - "email": "john@example.com", - "name": "John Smith", - "state": "active", - "created_at": "2012-05-23T08:00:58Z" - }, - "state": "opened", - "updated_at": "2012-07-12T13:43:19Z", - "created_at": "2012-06-28T12:58:06Z" - } + { + "state" : "opened", + "description" : "Ratione dolores corrupti mollitia soluta quia.", + "author" : { + "state" : "active", + "id" : 18, + "web_url" : "https://gitlab.example.com/u/eileen.lowe", + "name" : "Alexandra Bashirian", + "avatar_url" : null, + "username" : "eileen.lowe" + }, + "milestone" : { + "project_id" : 1, + "description" : "Ducimus nam enim ex consequatur cumque ratione.", + "state" : "closed", + "due_date" : null, + "iid" : 2, + "created_at" : "2016-01-04T15:31:39.996Z", + "title" : "v4.0", + "id" : 17, + "updated_at" : "2016-01-04T15:31:39.996Z" + }, + "project_id" : 1, + "assignee" : { + "state" : "active", + "id" : 1, + "name" : "Administrator", + "web_url" : "https://gitlab.example.com/u/root", + "avatar_url" : null, + "username" : "root" + }, + "updated_at" : "2016-01-04T15:31:51.081Z", + "id" : 76, + "title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.", + "created_at" : "2016-01-04T15:31:51.081Z", + "iid" : 6, + "labels" : [] + }, ] ``` ## List project issues -Get a list of project issues. This function accepts pagination parameters `page` and `per_page` -to return the list of project issues. +Get a list of a project's issues. ``` GET /projects/:id/issues @@ -102,67 +97,123 @@ GET /projects/:id/issues?milestone=1.0.0&state=opened GET /projects/:id/issues?iid=42 ``` -Parameters: +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `iid` | integer | no | Return the issue having the given `iid` | +| `state` | string | no | Return all issues or just those that are `opened` or `closed`| +| `labels` | string | no | Comma-separated list of label names | +| `milestone` | string| no | The milestone title | +| `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | +| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | + + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues +``` + +Example response: -- `id` (required) - The ID of a project -- `iid` (optional) - Return the issue having the given `iid` -- `state` (optional) - Return `all` issues or just those that are `opened` or `closed` -- `labels` (optional) - Comma-separated list of label names -- `milestone` (optional) - Milestone title -- `order_by` (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` -- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` +```json +[ + { + "project_id" : 4, + "milestone" : { + "due_date" : null, + "project_id" : 4, + "state" : "closed", + "description" : "Rerum est voluptatem provident consequuntur molestias similique ipsum dolor.", + "iid" : 3, + "id" : 11, + "title" : "v3.0", + "created_at" : "2016-01-04T15:31:39.788Z", + "updated_at" : "2016-01-04T15:31:39.788Z" + }, + "author" : { + "state" : "active", + "web_url" : "https://gitlab.example.com/u/root", + "avatar_url" : null, + "username" : "root", + "id" : 1, + "name" : "Administrator" + }, + "description" : "Omnis vero earum sunt corporis dolor et placeat.", + "state" : "closed", + "iid" : 1, + "assignee" : { + "avatar_url" : null, + "web_url" : "https://gitlab.example.com/u/lennie", + "state" : "active", + "username" : "lennie", + "id" : 9, + "name" : "Dr. Luella Kovacek" + }, + "labels" : [], + "id" : 41, + "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.", + "updated_at" : "2016-01-04T15:31:46.176Z", + "created_at" : "2016-01-04T15:31:46.176Z" + } +] +``` ## Single issue -Gets a single project issue. +Get a single project issue. ``` GET /projects/:id/issues/:issue_id ``` -Parameters: +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id`| integer | yes | The ID of a project's issue | -- `id` (required) - The ID of a project -- `issue_id` (required) - The ID of a project issue +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/41 +``` + +Example response: ```json { - "id": 42, - "iid": 3, - "project_id": 8, - "title": "Add user settings", - "description": "", - "labels": [ - "feature" - ], - "milestone": { - "id": 1, - "title": "v1.0", - "description": "", - "due_date": "2012-07-20", - "state": "closed", - "updated_at": "2012-07-04T13:42:48Z", - "created_at": "2012-07-04T13:42:48Z" - }, - "assignee": { - "id": 2, - "username": "jack_smith", - "email": "jack@example.com", - "name": "Jack Smith", - "state": "active", - "created_at": "2012-05-23T08:01:01Z" - }, - "author": { - "id": 1, - "username": "john_smith", - "email": "john@example.com", - "name": "John Smith", - "state": "active", - "created_at": "2012-05-23T08:00:58Z" - }, - "state": "opened", - "updated_at": "2012-07-12T13:43:19Z", - "created_at": "2012-06-28T12:58:06Z" + "project_id" : 4, + "milestone" : { + "due_date" : null, + "project_id" : 4, + "state" : "closed", + "description" : "Rerum est voluptatem provident consequuntur molestias similique ipsum dolor.", + "iid" : 3, + "id" : 11, + "title" : "v3.0", + "created_at" : "2016-01-04T15:31:39.788Z", + "updated_at" : "2016-01-04T15:31:39.788Z" + }, + "author" : { + "state" : "active", + "web_url" : "https://gitlab.example.com/u/root", + "avatar_url" : null, + "username" : "root", + "id" : 1, + "name" : "Administrator" + }, + "description" : "Omnis vero earum sunt corporis dolor et placeat.", + "state" : "closed", + "iid" : 1, + "assignee" : { + "avatar_url" : null, + "web_url" : "https://gitlab.example.com/u/lennie", + "state" : "active", + "username" : "lennie", + "id" : 9, + "name" : "Dr. Luella Kovacek" + }, + "labels" : [], + "id" : 41, + "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.", + "updated_at" : "2016-01-04T15:31:46.176Z", + "created_at" : "2016-01-04T15:31:46.176Z" } ``` @@ -170,57 +221,122 @@ Parameters: Creates a new project issue. +If the operation is successful, a status code of `200` and the newly-created +issue is returned. If an error occurs, an error number and a message explaining +the reason is returned. + ``` POST /projects/:id/issues ``` -Parameters: +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `title` | string | yes | The title of an issue | +| `description` | string | no | The description of an issue | +| `assignee_id` | integer | no | The ID of a user to assign issue | +| `milestone_id` | integer | no | The ID of a milestone to assign issue | +| `labels` | string | no | Comma-separated label names for an issue | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug +``` -- `id` (required) - The ID of a project -- `title` (required) - The title of an issue -- `description` (optional) - The description of an issue -- `assignee_id` (optional) - The ID of a user to assign issue -- `milestone_id` (optional) - The ID of a milestone to assign issue -- `labels` (optional) - Comma-separated label names for an issue +Example response: -If the operation is successful, 200 and the newly created issue is returned. -If an error occurs, an error number and a message explaining the reason is returned. +```json +{ + "project_id" : 4, + "id" : 84, + "created_at" : "2016-01-07T12:44:33.959Z", + "iid" : 14, + "title" : "Issues with auth", + "state" : "opened", + "assignee" : null, + "labels" : [ + "bug" + ], + "author" : { + "name" : "Alexandra Bashirian", + "avatar_url" : null, + "state" : "active", + "web_url" : "https://gitlab.example.com/u/eileen.lowe", + "id" : 18, + "username" : "eileen.lowe" + }, + "description" : null, + "updated_at" : "2016-01-07T12:44:33.959Z", + "milestone" : null +} +``` ## Edit issue -Updates an existing project issue. This function is also used to mark an issue as closed. +Updates an existing project issue. This call is also used to mark an issue as +closed. + +If the operation is successful, a code of `200` and the updated issue is +returned. If an error occurs, an error number and a message explaining the +reason is returned. ``` PUT /projects/:id/issues/:issue_id ``` -Parameters: +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of a project's issue | +| `title` | string | no | The title of an issue | +| `description` | string | no | The description of an issue | +| `assignee_id` | integer | no | The ID of a user to assign the issue to | +| `milestone_id` | integer | no | The ID of a milestone to assign the issue to | +| `labels` | string | no | Comma-separated label names for an issue | +| `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it | + +```bash +curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85?state_event=close +``` -- `id` (required) - The ID of a project -- `issue_id` (required) - The ID of a project's issue -- `title` (optional) - The title of an issue -- `description` (optional) - The description of an issue -- `assignee_id` (optional) - The ID of a user to assign issue -- `milestone_id` (optional) - The ID of a milestone to assign issue -- `labels` (optional) - Comma-separated label names for an issue -- `state_event` (optional) - The state event of an issue ('close' to close issue and 'reopen' to reopen it) +Example response: -If the operation is successful, 200 and the updated issue is returned. -If an error occurs, an error number and a message explaining the reason is returned. +```json +{ + "created_at" : "2016-01-07T12:46:01.410Z", + "author" : { + "name" : "Alexandra Bashirian", + "avatar_url" : null, + "username" : "eileen.lowe", + "id" : 18, + "state" : "active", + "web_url" : "https://gitlab.example.com/u/eileen.lowe" + }, + "state" : "closed", + "title" : "Issues with auth", + "project_id" : 4, + "description" : null, + "updated_at" : "2016-01-07T12:55:16.213Z", + "iid" : 15, + "labels" : [ + "bug" + ], + "id" : 85, + "assignee" : null, + "milestone" : null +} +``` ## Delete existing issue (**Deprecated**) -The function is deprecated and returns a `405 Method Not Allowed` error if called. An issue gets now closed and is done by calling `PUT /projects/:id/issues/:issue_id` with parameter `state_event` set to `close`. +This call is deprecated and returns a `405 Method Not Allowed` error if called. +An issue gets now closed and is done by calling +`PUT /projects/:id/issues/:issue_id` with the parameter `state_event` set to +`close`. See [edit issue](#edit-issue) for more details. ``` DELETE /projects/:id/issues/:issue_id ``` -Parameters: - -- `id` (required) - The project ID -- `issue_id` (required) - The ID of the issue - ## Comments on issues -Comments are done via the notes resource. +Comments are done via the [notes](notes.md) resource. -- cgit v1.2.1 From cfb0628d76575ea1ae8671e7e3d8896fbf38a5c8 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 18 Jan 2016 09:16:14 +0100 Subject: Refactor deploy_keys API documentation [ci skip] --- doc/api/deploy_keys.md | 74 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 12 deletions(-) diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md index e4492fc609c..9da1fe22e61 100644 --- a/doc/api/deploy_keys.md +++ b/doc/api/deploy_keys.md @@ -8,9 +8,15 @@ Get a list of a project's deploy keys. GET /projects/:id/keys ``` -Parameters: +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/keys" +``` -- `id` (required) - The ID of the project +Example response: ```json [ @@ -39,8 +45,16 @@ GET /projects/:id/keys/:key_id Parameters: -- `id` (required) - The ID of the project -- `key_id` (required) - The ID of the deploy key +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | +| `key_id` | integer | yes | The ID of the deploy key | + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/keys/11" +``` + +Example response: ```json { @@ -54,17 +68,34 @@ Parameters: ## Add deploy key Creates a new deploy key for a project. -If deploy key already exists in another project - it will be joined to project but only if original one was is accessible by same user + +If the deploy key already exists in another project, it will be joined to current +project only if original one was is accessible by the same user. ``` POST /projects/:id/keys ``` -Parameters: +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | +| `title` | string | yes | New deploy key's title | +| `key` | string | yes | New deploy key | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -H "Content-Type: application/json" --data '{"title": "My deploy key", "key": "ssh-rsa AAAA..."}' "https://gitlab.example.com/api/v3/projects/5/keys/" +``` -- `id` (required) - The ID of the project -- `title` (required) - New deploy key's title -- `key` (required) - New deploy key +Example response: + +```json +{ + "key" : "ssh-rsa AAAA...", + "id" : 12, + "title" : "My deploy key", + "created_at" : "2015-08-29T12:44:31.550Z" +} +``` ## Delete deploy key @@ -74,7 +105,26 @@ Delete a deploy key from a project DELETE /projects/:id/keys/:key_id ``` -Parameters: +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | +| `key_id` | integer | yes | The ID of the deploy key | + +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/keys/13" +``` -- `id` (required) - The ID of the project -- `key_id` (required) - The ID of the deploy key +Example response: + +```json +{ + "updated_at" : "2015-08-29T12:50:57.259Z", + "key" : "ssh-rsa AAAA...", + "public" : false, + "title" : "My deploy key", + "user_id" : null, + "created_at" : "2015-08-29T12:50:57.259Z", + "fingerprint" : "6a:33:1f:74:51:c0:39:81:79:ec:7a:31:f8:40:20:43", + "id" : 13 +} +``` -- cgit v1.2.1 From 981a6865f2fa6185eb3f9bc746f5fb60badf8f09 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 18 Jan 2016 09:41:42 +0100 Subject: Refactor labels API documentation [ci skip] --- doc/api/labels.md | 152 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 111 insertions(+), 41 deletions(-) diff --git a/doc/api/labels.md b/doc/api/labels.md index de41f35d284..6496ffe9fd1 100644 --- a/doc/api/labels.md +++ b/doc/api/labels.md @@ -2,83 +2,153 @@ ## List labels -Get all labels for given project. +Get all labels for a given project. ``` GET /projects/:id/labels ``` +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/labels +``` + +Example response: + ```json [ - { - "name": "Awesome", - "color": "#DD10AA" - }, - { - "name": "Documentation", - "color": "#1E80DD" - }, - { - "name": "Feature", - "color": "#11FF22" - }, - { - "name": "Bug", - "color": "#EE1122" - } + { + "name" : "bug", + "color" : "#d9534f" + }, + { + "color" : "#d9534f", + "name" : "confirmed" + }, + { + "name" : "critical", + "color" : "#d9534f" + }, + { + "color" : "#428bca", + "name" : "discussion" + }, + { + "name" : "documentation", + "color" : "#f0ad4e" + }, + { + "color" : "#5cb85c", + "name" : "enhancement" + }, + { + "color" : "#428bca", + "name" : "suggestion" + }, + { + "color" : "#f0ad4e", + "name" : "support" + } ] ``` ## Create a new label -Creates a new label for given repository with given name and color. +Creates a new label for the given repository with the given name and color. + +It returns 200 if the label was successfully created, 400 for wrong parameters +and 409 if the label already exists. ``` POST /projects/:id/labels ``` -Parameters: +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | +| `name` | string | yes | The name of the label | +| `color` | string | yes | The color of the label in 6-digit hex notation with leading `#` sign | + +```bash +curl --data "name=feature&color=#5843AD" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels" +``` -- `id` (required) - The ID of a project -- `name` (required) - The name of the label -- `color` (required) - Color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) +Example response: -It returns 200 and the newly created label, if the operation succeeds. -If the label already exists, 409 and an error message is returned. -If label parameters are invalid, 400 and an explaining error message is returned. +```json +{ + "name" : "feature", + "color" : "#5843AD" +} +``` ## Delete a label -Deletes a label given by its name. +Deletes a label with a given name. + +It returns 200 if the label was successfully deleted, 400 for wrong parameters +and 404 if the label does not exist. +In case of an error, an additional error message is returned. ``` DELETE /projects/:id/labels ``` -- `id` (required) - The ID of a project -- `name` (required) - The name of the label to be deleted +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | +| `name` | string | yes | The name of the label | -It returns 200 if the label successfully was deleted, 400 for wrong parameters -and 404 if the label does not exist. -In case of an error, additionally an error message is returned. +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels?name=bug" +``` + +Example response: + +```json +{ + "title" : "feature", + "color" : "#5843AD", + "updated_at" : "2015-11-03T21:22:30.737Z", + "template" : false, + "project_id" : 1, + "created_at" : "2015-11-03T21:22:30.737Z", + "id" : 9 +} +``` ## Edit an existing label -Updates an existing label with new name or now color. At least one parameter +Updates an existing label with new name or new color. At least one parameter is required, to update the label. +It returns 200 if the label was successfully deleted, 400 for wrong parameters +and 404 if the label does not exist. +In case of an error, an additional error message is returned. + ``` PUT /projects/:id/labels ``` -Parameters: +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | +| `name` | string | yes | The name of the existing label | +| `new_name` | string | yes if `color` if not provided | The new name of the label | +| `color` | string | yes if `new_name` is not provided | The new color of the label in 6-digit hex notation with leading `#` sign | + +```bash +curl -X PUT --data "name=documentation&new_name=docs&color=#8E44AD" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels" +``` -- `id` (required) - The ID of a project -- `name` (required) - The name of the existing label -- `new_name` (optional) - The new name of the label -- `color` (optional) - New color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) +Example response: -On success, this method returns 200 with the updated label. -If required parameters are missing or parameters are invalid, 400 is returned. -If the label to be updated is missing, 404 is returned. -In case of an error, additionally an error message is returned. +```json +{ + "color" : "#8E44AD", + "name" : "docs" +} +``` -- cgit v1.2.1 From ddc8b91a5f89c1bd1b4bed955231ae286420d9d2 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 18 Jan 2016 09:42:44 +0100 Subject: Refactor namespaces API documentation [ci skip] --- doc/api/namespaces.md | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md index 7b3238441f6..68ed8bd3152 100644 --- a/doc/api/namespaces.md +++ b/doc/api/namespaces.md @@ -1,13 +1,22 @@ # Namespaces +Namespaces account for usernames and groupnames. + +[Pagination](README.md#pagination) is used. + ## List namespaces -Get a list of namespaces. (As user: my namespaces, as admin: all namespaces) +Get a list of namespaces. As a user, your namespaces are listed whereas if you +are an administrator you get a list of all namespaces in the GitLab instance. ``` GET /namespaces ``` +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces +``` + ```json [ { @@ -23,22 +32,28 @@ GET /namespaces ] ``` -You can search for namespaces by name or path, see below. - ## Search for namespace -Get all namespaces that match your string in their name or path. +Get all namespaces that match a string in their name or path. ``` GET /namespaces?search=foobar ``` +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `search` | string | no | Returns a list of namespaces the user is authorized to see based on the search criteria | + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces?search=twitter +``` + ```json [ { - "id": 1, - "path": "user1", - "kind": "user" + "id": 4, + "path": "twitter", + "kind": "group" } ] ``` -- cgit v1.2.1 From 585e7f6e35c8b40ac1d79b06da399bbff7181bdc Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 18 Jan 2016 09:44:19 +0100 Subject: Refactor settings API documentation [ci skip] --- doc/api/settings.md | 98 +++++++++++++++++++++++++++++------------------------ 1 file changed, 54 insertions(+), 44 deletions(-) diff --git a/doc/api/settings.md b/doc/api/settings.md index 96867c67915..001de76c7af 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -1,67 +1,77 @@ # Application settings -This API allows you to read and modify GitLab instance application settings. +These API calls allow you to read and modify GitLab instance application +settings as appear in `/admin/application_settings`. You have to be an +administrator in order to perform this action. +## Get current application settings -## Get current application settings: +List the current application settings of the GitLab instance. ``` GET /application/settings ``` +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings +``` + +Example response: + ```json { - "id": 1, - "default_projects_limit": 10, - "signup_enabled": true, - "signin_enabled": true, - "gravatar_enabled": true, - "sign_in_text": "", - "created_at": "2015-06-12T15:51:55.432Z", - "updated_at": "2015-06-30T13:22:42.210Z", - "home_page_url": "", - "default_branch_protection": 2, - "twitter_sharing_enabled": true, - "restricted_visibility_levels": [], - "max_attachment_size": 10, - "session_expire_delay": 10080, - "default_project_visibility": 0, - "default_snippet_visibility": 0, - "restricted_signup_domains": [], - "user_oauth_applications": true, - "after_sign_out_path": "" + "default_projects_limit" : 10, + "signup_enabled" : true, + "id" : 1, + "default_branch_protection" : 2, + "restricted_visibility_levels" : [], + "signin_enabled" : true, + "twitter_sharing_enabled" : true, + "after_sign_out_path" : null, + "max_attachment_size" : 10, + "user_oauth_applications" : true, + "updated_at" : "2016-01-04T15:44:55.176Z", + "session_expire_delay" : 10080, + "home_page_url" : null, + "default_snippet_visibility" : 0, + "restricted_signup_domains" : [], + "created_at" : "2016-01-04T15:44:55.176Z", + "default_project_visibility" : 0, + "gravatar_enabled" : true, + "sign_in_text" : null } ``` -## Change application settings: - - +## Change application settings ``` PUT /application/settings ``` -Parameters: - -- `default_projects_limit` - project limit per user -- `signup_enabled` - enable registration -- `signin_enabled` - enable login via GitLab account -- `gravatar_enabled` - enable gravatar -- `sign_in_text` - text on login page -- `home_page_url` - redirect to this URL when not logged in -- `default_branch_protection` - determine if developers can push to master -- `twitter_sharing_enabled` - allow users to share project creation in twitter -- `restricted_visibility_levels` - restrict certain visibility levels -- `max_attachment_size` - limit attachment size -- `session_expire_delay` - session lifetime -- `default_project_visibility` - what visibility level new project receives -- `default_snippet_visibility` - what visibility level new snippet receives -- `restricted_signup_domains` - force people to use only corporate emails for signup -- `user_oauth_applications` - allow users to create oauth applications -- `after_sign_out_path` - where redirect user after logout +| Attribute | Type | Required | Description | +| --------- | ---- | :------: | ----------- | +| `default_projects_limit` | integer | no | Project limit per user. Default is `10` | +| `signup_enabled` | boolean | no | Enable registration. Default is `true`. | +| `signin_enabled` | boolean | no | Enable login via a GitLab account. Default is `true`. | +| `gravatar_enabled` | boolean | no | Enable Gravatar | +| `sign_in_text` | string | no | Text on login page | +| `home_page_url` | string | no | Redirect to this URL when not logged in | +| `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push or delete the branch)_, `1` _(partially protected, developers can push new commits, but cannot force push or delete the branch, masters can do anything)_ or `2` _(fully protected, developers cannot push new commits, force push or delete the branch, masters can do anything)_ as a parameter. Default is `1`. | +| `twitter_sharing_enabled` | boolean | no | Allow users to share project creation on Twitter | +| `restricted_visibility_levels` | array of integers | no | Selected levels cannot be used by non-admin users for projects or snippets. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is null which means there is no restriction. | +| `max_attachment_size` | integer | no | Limit attachment size in MB | +| `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes | +| `default_project_visibility` | integer | no | What visibility level new projects receive. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is `0`.| +| `default_snippet_visibility` | integer | no | What visibility level new snippets receive. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is `0`.| +| `restricted_signup_domains` | array of strings | no | Force people to use only corporate emails for sign-up. Default is null, meaning there is no restriction. | +| `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider | +| `after_sign_out_path` | string | no | Where to redirect users after logout | -All parameters are optional. You can send only one that you want to change. +```bash +curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1 +``` +Example response: ```json { @@ -79,7 +89,7 @@ All parameters are optional. You can send only one that you want to change. "restricted_visibility_levels": [], "max_attachment_size": 10, "session_expire_delay": 10080, - "default_project_visibility": 0, + "default_project_visibility": 1, "default_snippet_visibility": 0, "restricted_signup_domains": [], "user_oauth_applications": true, -- cgit v1.2.1 From 3db8cfefc79de501aefc447d8f4360b9f6656196 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 18 Jan 2016 09:45:50 +0100 Subject: Refactor system_hooks API documentation [ci skip] --- doc/api/system_hooks.md | 96 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 73 insertions(+), 23 deletions(-) diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md index f9637d8a6c4..f539f64488b 100644 --- a/doc/api/system_hooks.md +++ b/doc/api/system_hooks.md @@ -2,39 +2,58 @@ All methods require admin authorization. -The URL endpoint of the system hooks can be configured in [the admin area under hooks](/admin/hooks). +The URL endpoint of the system hooks can also be configured using the UI in +[the admin area under hooks](/admin/hooks). ## List system hooks -Get list of system hooks +Get a list of all system hooks. ``` GET /hooks ``` -Parameters: +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks +``` -- **none** +Example response: ```json [ - { - "id": 3, - "url": "http://example.com/hook", - "created_at": "2013-10-02T10:15:31Z" - } + { + "id" : 1, + "url" : "https://gitlab.example.com/hook", + "created_at" : "2015-11-04T20:07:35.874Z" + } ] ``` -## Add new system hook hook +## Add new system hook ``` POST /hooks ``` -Parameters: +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `url` | string | yes | The hook URL | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/hooks?url=https://gitlab.example.com/hook" +``` -- `url` (required) - The hook URL +Example response: + +```json +[ + { + "id" : 2, + "url" : "https://gitlab.example.com/hook", + "created_at" : "2015-11-04T20:07:35.874Z" + } +] +``` ## Test system hook @@ -42,29 +61,60 @@ Parameters: GET /hooks/:id ``` -Parameters: +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the hook | -- `id` (required) - The ID of hook +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2 +``` + +Example response: ```json { - "event_name": "project_create", - "name": "Ruby", - "path": "ruby", - "project_id": 1, - "owner_name": "Someone", - "owner_email": "example@gitlabhq.com" + "project_id" : 1, + "owner_email" : "example@gitlabhq.com", + "owner_name" : "Someone", + "name" : "Ruby", + "path" : "ruby", + "event_name" : "project_create" } ``` ## Delete system hook -Deletes a system hook. This is an idempotent API function and returns `200 OK` even if the hook is not available. If the hook is deleted it is also returned as JSON. +Deletes a system hook. This is an idempotent API function and returns `200 OK` +even if the hook is not available. If the hook is deleted a JSON object is +returned. ``` DELETE /hooks/:id ``` -Parameters: +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the hook | + +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2 +``` + +Example response: -- `id` (required) - The ID of hook +```json +{ + "note_events" : false, + "project_id" : null, + "enable_ssl_verification" : true, + "url" : "https://gitlab.example.com/hook", + "updated_at" : "2015-11-04T20:12:15.931Z", + "issues_events" : false, + "merge_requests_events" : false, + "created_at" : "2015-11-04T20:12:15.931Z", + "service_id" : null, + "id" : 2, + "push_events" : true, + "tag_push_events" : false +} +``` -- cgit v1.2.1 From 9eeda3da3dfa3b1e328ab139bd2b0a389dea5ef1 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 18 Jan 2016 09:48:19 +0100 Subject: Remove relative link It wouldn't work on doc.gitlab.com [ci skip] --- doc/api/system_hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md index f539f64488b..b605f3540f3 100644 --- a/doc/api/system_hooks.md +++ b/doc/api/system_hooks.md @@ -3,7 +3,7 @@ All methods require admin authorization. The URL endpoint of the system hooks can also be configured using the UI in -[the admin area under hooks](/admin/hooks). +the admin area under hooks(`/admin/hooks`). ## List system hooks -- cgit v1.2.1 From 60752b6ec8d6a3a9ba368a66a69ea7e93bb104ef Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 18 Jan 2016 10:10:33 +0100 Subject: Refactor README API documentation [ci skip] --- doc/api/README.md | 244 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 147 insertions(+), 97 deletions(-) diff --git a/doc/api/README.md b/doc/api/README.md index 2fa177ff4dd..51cd67f47f1 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -27,16 +27,15 @@ - [Build triggers](build_triggers.md) - [Build Variables](build_variables.md) -## Clients - -Find API Clients for GitLab [on our website](https://about.gitlab.com/applications/#api-clients). -You can use [GitLab as an OAuth2 client](oauth2.md) to make API calls. - -## Introduction +## Authentication -All API requests require authentication. You need to pass a `private_token` parameter by URL or header. If passed as header, the header name must be "PRIVATE-TOKEN" (capital and with dash instead of underscore). You can find or reset your private token in your profile. +All API requests require authentication. You need to pass a `private_token` +parameter by URL or header. If passed as a header, the header name must be +**PRIVATE-TOKEN** (capital and with dash instead of underscore). You can find +or reset your private token in your account page (`/profile/account`). -If no, or an invalid, `private_token` is provided then an error message will be returned with status code 401: +If `private_token` is invalid or omitted, then an error message will be +returned with status code `401`: ```json { @@ -44,71 +43,82 @@ If no, or an invalid, `private_token` is provided then an error message will be } ``` -API requests should be prefixed with `api` and the API version. The API version is defined in `lib/api.rb`. +API requests should be prefixed with `api` and the API version. The API version +is defined in `lib/api.rb`. Example of a valid API request: -``` -GET http://example.com/api/v3/projects?private_token=QVy1PB7sTxfy4pqfZM1U +```shell +GET https://gitlab.example.com/api/v3/projects?private_token=9koXpg98eAheJpvBs5tK ``` -Example for a valid API request using curl and authentication via header: +Example of a valid API request using curl and authentication via header: -``` -curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" "http://example.com/api/v3/projects" +```shell +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects" ``` The API uses JSON to serialize data. You don't need to specify `.json` at the end of API URL. ## Authentication with OAuth2 token -Instead of the private_token you can transmit the OAuth2 access token as a header or as a parameter. +Instead of the `private_token` you can transmit the OAuth2 access token as a +header or as a parameter. -### OAuth2 token (as a parameter) +Example of OAuth2 token as a parameter: -``` -curl https://localhost:3000/api/v3/user?access_token=OAUTH-TOKEN +```shell +curl https://gitlab.example.com/api/v3/user?access_token=OAUTH-TOKEN ``` -### OAuth2 token (as a header) +Example of OAuth2 token as a header: -``` -curl -H "Authorization: Bearer OAUTH-TOKEN" https://localhost:3000/api/v3/user +```shell +curl -H "Authorization: Bearer OAUTH-TOKEN" https://example.com/api/v3/user ``` Read more about [GitLab as an OAuth2 client](oauth2.md). ## Status codes -The API is designed to return different status codes according to context and action. In this way if a request results in an error the caller is able to get insight into what went wrong, e.g. status code `400 Bad Request` is returned if a required attribute is missing from the request. The following list gives an overview of how the API functions generally behave. - -API request types: - -- `GET` requests access one or more resources and return the result as JSON -- `POST` requests return `201 Created` if the resource is successfully created and return the newly created resource as JSON -- `GET`, `PUT` and `DELETE` return `200 OK` if the resource is accessed, modified or deleted successfully, the (modified) result is returned as JSON -- `DELETE` requests are designed to be idempotent, meaning a request a resource still returns `200 OK` even it was deleted before or is not available. The reasoning behind it is the user is not really interested if the resource existed before or not. - -The following list shows the possible return codes for API requests. - -Return values: - -- `200 OK` - The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON -- `201 Created` - The `POST` request was successful and the resource is returned as JSON -- `400 Bad Request` - A required attribute of the API request is missing, e.g. the title of an issue is not given -- `401 Unauthorized` - The user is not authenticated, a valid user token is necessary, see above -- `403 Forbidden` - The request is not allowed, e.g. the user is not allowed to delete a project -- `404 Not Found` - A resource could not be accessed, e.g. an ID for a resource could not be found -- `405 Method Not Allowed` - The request is not supported -- `409 Conflict` - A conflicting resource already exists, e.g. creating a project with a name that already exists -- `422 Unprocessable` - The entity could not be processed -- `500 Server Error` - While handling the request something went wrong on the server side +The API is designed to return different status codes according to context and +action. This way if a request results in an error, the caller is able to get +insight into what went wrong. + +The following table gives an overview of how the API functions generally behave. + +| Request type | Explanation | +| ------------ | ----------- | +| `GET` | Access one or more resources and return the result as JSON. | +| `POST` | Return `201 Created` if the resource is successfully created and return the newly created resource as JSON. | +| `GET` / `PUT` / `DELETE` | Return `200 OK` if the resource is accessed, modified or deleted successfully. The (modified) result is returned as JSON. | +| `DELETE` | Designed to be idempotent, meaning a request to a resource still returns `200 OK` even it was deleted before or is not available. The reasoning behind it is the user is not really interested if the resource existed before or not. | + +The following table shows the possible return codes for API requests. + +| Return values | Explanation | +| ------------- | ----------- | +| `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. | +| `201 Created` | The `POST` request was successful and the resource is returned as JSON. | +| `400 Bad Request` | A required attribute of the API request is missing, e.g. the title of an issue is not given. | +| `401 Unauthorized` | The user is not authenticated, a valid [user token](#authentication) is necessary. | +| `403 Forbidden` | The request is not allowed, e.g. the user is not allowed to delete a project. | +| `404 Not Found` | A resource could not be accessed, e.g. an ID for a resource could not be found. | +| `405 Method Not Allowed` | The request is not supported. | +| `409 Conflict` | A conflicting resource already exists, e.g. creating a project with a name that already exists. | +| `422 Unprocessable` | The entity could not be processed. | +| `500 Server Error` | While handling the request something went wrong on the server side. | ## Sudo -All API requests support performing an api call as if you were another user, if your private token is for an administration account. You need to pass `sudo` parameter by URL or header with an id or username of the user you want to perform the operation as. If passed as header, the header name must be "SUDO" (capitals). +All API requests support performing an API call as if you were another user, +provided your private token is from an administration account. You need to pass +the `sudo` parameter by URL or header with an ID or username of the user you +want to perform the operation as. If passed as a header, the header name must +be **SUDO** (capitals). -If a non administrative `private_token` is provided then an error message will be returned with status code 403: +If a non administrative `private_token` is provided, then an error message will +be returned with status code `403`: ```json { @@ -116,7 +126,8 @@ If a non administrative `private_token` is provided then an error message will b } ``` -If the sudo user id or username cannot be found then an error message will be returned with status code 404: +If the sudo user id or username cannot be found, an error message will be +returned with status code `404`: ```json { @@ -124,94 +135,133 @@ If the sudo user id or username cannot be found then an error message will be re } ``` -Example of a valid API with sudo request: +Example of a valid API call with sudo request providing a username and an ID +respectively: -``` -GET http://example.com/api/v3/projects?private_token=QVy1PB7sTxfy4pqfZM1U&sudo=username +```shell +GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=username ``` -``` -GET http://example.com/api/v3/projects?private_token=QVy1PB7sTxfy4pqfZM1U&sudo=23 +```shell +GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23 ``` -Example for a valid API request with sudo using curl and authentication via header: +Example of a valid API request with sudo using curl and authentication via +header: -``` -curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" --header "SUDO: username" "http://example.com/api/v3/projects" +```shell +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: username" "https://gitlab.example.com/api/v3/projects" ``` -``` -curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" --header "SUDO: 23" "http://example.com/api/v3/projects" +```shell +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: 23" "https://gitlab.example.com/api/v3/projects" ``` ## Pagination -When listing resources you can pass the following parameters: +Sometimes the returned result will span across many lines. When listing +resources you can pass the following parameters. -- `page` (default: `1`) - page number -- `per_page` (default: `20`, max: `100`) - number of items to list per page +| Parameter | Description | +| --------- | ----------- | +| `page` | Page number (default: `1`). | +| `per_page`| Number of items to list per page (default: `20`, max: `100`). | -[Link headers](http://www.w3.org/wiki/LinkHeader) are send back with each response. These have `rel` prev/next/first/last and contain the relevant URL. Please use these instead of generating your own URLs. +In the example below, we list 50 [namespaces](namespaces.md) per page. + +```bash +curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/namespaces?per_page=50 +``` + +[Link headers](http://www.w3.org/wiki/LinkHeader) are sent back with each +response. These have `rel` prev/next/first/last and contain the relevant URL. +Please use these instead of generating your own URLs. ## id vs iid -When you work with API you may notice two similar fields in api entities: id and iid. The main difference between them is scope. Example: +When you work with the API, you may notice two similar fields in API entities: +`id` and `iid`. The main difference between them is scope. + +For example, an issue might have `id: 46` and `iid: 5`. -Issue: +| Parameter | Description | +| --------- | ----------- | +| id | is unique across all issues and is used for any API call. | +| iid | is unique only in scope of a single project. When you browse issues or merge requests with Web UI, you see the `iid`. | - id: 46 - iid: 5 +That means that if you want to get an issue via the API you should use: + +```bash +https://gitlab.example.com/api/v3/projects/42/issues/:id.json +``` -- id - is unique across all issues. It's used for any api call. -- iid - is unique only in scope of a single project. When you browse issues or merge requests with Web UI, you see iid. +On the other hand, if you want to create a link to a web page you should use: -So if you want to get issue with api you use `http://host/api/v3/.../issues/:id.json`. But when you want to create a link to web page - use `http:://host/project/issues/:iid.json` +```bash +https://gitlab.example.com/api/v3/projects/42/issues/:iid.json +``` ## Data validation and error reporting -When working with the API you may encounter validation errors. In such case, the API will answer with an HTTP `400` status. +When working with the API you may encounter validation errors. In such case, +the API will answer with an HTTP `400` status. + Such errors appear in two cases: -* A required attribute of the API request is missing, e.g. the title of an issue is not given -* An attribute did not pass the validation, e.g. user bio is too long +- A required attribute of the API request is missing, e.g. the title of an + issue is not given +- An attribute did not pass the validation, e.g. user bio is too long When an attribute is missing, you will get something like: - HTTP/1.1 400 Bad Request - Content-Type: application/json - - { - "message":"400 (Bad request) \"title\" not given" - } - -When a validation error occurs, error messages will be different. They will hold all details of validation errors: +``` +HTTP/1.1 400 Bad Request +Content-Type: application/json +{ + "message":"400 (Bad request) \"title\" not given" +} +``` - HTTP/1.1 400 Bad Request - Content-Type: application/json +When a validation error occurs, error messages will be different. They will +hold all details of validation errors: - { - "message": { - "bio": [ - "is too long (maximum is 255 characters)" - ] - } +``` +HTTP/1.1 400 Bad Request +Content-Type: application/json +{ + "message": { + "bio": [ + "is too long (maximum is 255 characters)" + ] } +} +``` -This makes error messages more machine-readable. The format can be described as follow: +This makes error messages more machine-readable. The format can be described as +follows: - { - "message": { +```json +{ + "message": { + "": [ + "", + "", + ... + ], + "": { "": [ "", "", ... ], - "": { - "": [ - "", - "", - ... - ], - } } } +} +``` + +## Clients + +There are many unofficial GitLab API Clients for most of the popular +programming languages. Visit the [GitLab website][] for a complete list. + +[GitLab website]: https://about.gitlab.com/applications/#api-clients -- cgit v1.2.1 From 958dd277775146fb35fc53ae7f6b6c30a96deb50 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 18 Jan 2016 11:38:54 +0100 Subject: Clean-up API README [ci skip] - Fix some curl examples - Fix examples for id vs iid --- doc/api/README.md | 69 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/doc/api/README.md b/doc/api/README.md index 0b902b9b2b9..2b6466a9e57 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -1,7 +1,13 @@ # GitLab API +Automate GitLab via a simple and powerful API. All definitions can be found +under [/lib/api](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/api). + ## Resources +Documentation for various API resources can be found separately in the +following locations: + - [Users](users.md) - [Session](session.md) - [Projects](projects.md) including setting Webhooks @@ -44,7 +50,7 @@ returned with status code `401`: ``` API requests should be prefixed with `api` and the API version. The API version -is defined in `lib/api.rb`. +is defined in [`lib/api.rb`][lib-api-url]. Example of a valid API request: @@ -52,13 +58,14 @@ Example of a valid API request: GET https://gitlab.example.com/api/v3/projects?private_token=9koXpg98eAheJpvBs5tK ``` -Example of a valid API request using curl and authentication via header: +Example of a valid API request using cURL and authentication via header: ```shell curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects" ``` -The API uses JSON to serialize data. You don't need to specify `.json` at the end of API URL. +The API uses JSON to serialize data. You don't need to specify `.json` at the +end of an API URL. ## Authentication with OAuth2 token @@ -82,21 +89,21 @@ Read more about [GitLab as an OAuth2 client](oauth2.md). ## Status codes The API is designed to return different status codes according to context and -action. This way if a request results in an error, the caller is able to get +action. This way, if a request results in an error, the caller is able to get insight into what went wrong. The following table gives an overview of how the API functions generally behave. -| Request type | Explanation | +| Request type | Description | | ------------ | ----------- | | `GET` | Access one or more resources and return the result as JSON. | | `POST` | Return `201 Created` if the resource is successfully created and return the newly created resource as JSON. | | `GET` / `PUT` / `DELETE` | Return `200 OK` if the resource is accessed, modified or deleted successfully. The (modified) result is returned as JSON. | -| `DELETE` | Designed to be idempotent, meaning a request to a resource still returns `200 OK` even it was deleted before or is not available. The reasoning behind it is the user is not really interested if the resource existed before or not. | +| `DELETE` | Designed to be idempotent, meaning a request to a resource still returns `200 OK` even it was deleted before or is not available. The reasoning behind this, is that the user is not really interested if the resource existed before or not. | The following table shows the possible return codes for API requests. -| Return values | Explanation | +| Return values | Description | | ------------- | ----------- | | `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. | | `201 Created` | The `POST` request was successful and the resource is returned as JSON. | @@ -107,15 +114,15 @@ The following table shows the possible return codes for API requests. | `405 Method Not Allowed` | The request is not supported. | | `409 Conflict` | A conflicting resource already exists, e.g. creating a project with a name that already exists. | | `422 Unprocessable` | The entity could not be processed. | -| `500 Server Error` | While handling the request something went wrong on the server side. | +| `500 Server Error` | While handling the request something went wrong server-side. | ## Sudo All API requests support performing an API call as if you were another user, -provided your private token is from an administration account. You need to pass -the `sudo` parameter by URL or header with an ID or username of the user you -want to perform the operation as. If passed as a header, the header name must -be **SUDO** (capitals). +provided your private token is from an administrator account. You need to pass +the `sudo` parameter either by URL or a header with an ID/username of the user +you want to perform the operation as. If passed as a header, the header name +must be **SUDO** (capitals). If a non administrative `private_token` is provided, then an error message will be returned with status code `403`: @@ -135,22 +142,24 @@ returned with status code `404`: } ``` -Example of a valid API call with sudo request providing a username and an ID -respectively: +--- + +Example of a valid API call and a request using cURL with sudo request, +providing a username: ```shell GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=username ``` ```shell -GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: username" "https://gitlab.example.com/api/v3/projects" ``` -Example of a valid API request with sudo using curl and authentication via -header: +Example of a valid API call and a request using cURL with sudo request, +providing an ID: ```shell -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: username" "https://gitlab.example.com/api/v3/projects" +GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23 ``` ```shell @@ -164,8 +173,8 @@ resources you can pass the following parameters. | Parameter | Description | | --------- | ----------- | -| `page` | Page number (default: `1`). | -| `per_page`| Number of items to list per page (default: `20`, max: `100`). | +| `page` | Page number (default: `1`) | +| `per_page`| Number of items to list per page (default: `20`, max: `100`) | In the example below, we list 50 [namespaces](namespaces.md) per page. @@ -230,19 +239,20 @@ For example, an issue might have `id: 46` and `iid: 5`. | Parameter | Description | | --------- | ----------- | -| id | is unique across all issues and is used for any API call. | -| iid | is unique only in scope of a single project. When you browse issues or merge requests with Web UI, you see the `iid`. | +| `id` | Is unique across all issues and is used for any API call | +| `iid` | Is unique only in scope of a single project. When you browse issues or merge requests with the Web UI, you see the `iid` | -That means that if you want to get an issue via the API you should use: +That means that if you want to get an issue via the API you should use the `id`: ```bash -https://gitlab.example.com/api/v3/projects/42/issues/:id.json +GET /projects/42/issues/:id ``` -On the other hand, if you want to create a link to a web page you should use: +On the other hand, if you want to create a link to a web page you should use +the `iid`: ```bash -https://gitlab.example.com/api/v3/projects/42/issues/:iid.json +GET /projects/42/issues/:iid ``` ## Data validation and error reporting @@ -258,7 +268,7 @@ Such errors appear in two cases: When an attribute is missing, you will get something like: -``` +```json HTTP/1.1 400 Bad Request Content-Type: application/json { @@ -269,7 +279,7 @@ Content-Type: application/json When a validation error occurs, error messages will be different. They will hold all details of validation errors: -``` +```json HTTP/1.1 400 Bad Request Content-Type: application/json { @@ -308,4 +318,5 @@ follows: There are many unofficial GitLab API Clients for most of the popular programming languages. Visit the [GitLab website][] for a complete list. -[GitLab website]: https://about.gitlab.com/applications/#api-clients +[GitLab website]: https://about.gitlab.com/applications/#api-clients "Clients using the GitLab API" +[lib-api-url]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/api/api.rb -- cgit v1.2.1 From 2ffc438efb81e2bbfe83c68860345e5902339bac Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 18 Jan 2016 11:45:04 +0100 Subject: Use plaintext instead of json in code blocks [ci skip] --- doc/api/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api/README.md b/doc/api/README.md index 2b6466a9e57..a9bfd2f7ebc 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -268,7 +268,7 @@ Such errors appear in two cases: When an attribute is missing, you will get something like: -```json +``` HTTP/1.1 400 Bad Request Content-Type: application/json { @@ -279,7 +279,7 @@ Content-Type: application/json When a validation error occurs, error messages will be different. They will hold all details of validation errors: -```json +``` HTTP/1.1 400 Bad Request Content-Type: application/json { -- cgit v1.2.1 From 790c68680893b333c0a9bedca7c528147b8417b1 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 18 Jan 2016 16:55:14 +0100 Subject: Added documentation for GitLab Metrics --- doc/README.md | 7 +++ doc/metrics/gitlab_configuration.md | 12 +++++ doc/metrics/influxdb_configuration.md | 96 +++++++++++++++++++++++++++++++++++ doc/metrics/influxdb_schema.md | 75 +++++++++++++++++++++++++++ doc/metrics/introduction.md | 55 ++++++++++++++++++++ 5 files changed, 245 insertions(+) create mode 100644 doc/metrics/gitlab_configuration.md create mode 100644 doc/metrics/influxdb_configuration.md create mode 100644 doc/metrics/influxdb_schema.md create mode 100644 doc/metrics/introduction.md diff --git a/doc/README.md b/doc/README.md index 7d4f84857e0..b9ee7de5349 100644 --- a/doc/README.md +++ b/doc/README.md @@ -49,6 +49,13 @@ - [Test Clojure applications](ci/examples/test-clojure-application.md) - Help your favorite programming language and GitLab by sending a merge request with a guide for that language. +## GitLab Metrics + +- [Introduction](metrics/introduction.md) +- [GitLab Configuration](metrics/gitlab_configuration.md) +- [InfluxDB Configuration](metrics/influxdb_configuration.md) +- [InfluxDB Schema](metrics/influxdb_schema.md) + ## Administrator documentation - [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when web hooks aren't enough. diff --git a/doc/metrics/gitlab_configuration.md b/doc/metrics/gitlab_configuration.md new file mode 100644 index 00000000000..3b44b4394ff --- /dev/null +++ b/doc/metrics/gitlab_configuration.md @@ -0,0 +1,12 @@ +# GitLab Configuration + +By default GitLab Metrics is disabled. To enable GitLab Metrics and change any +of its settings open a web browser and navigate to +`http://YOUR_GITLAB_HOST/admin/application_settings`, the settings can be found +in the "Metrics" section. A restart of all GitLab processes is required for any +changes to take effect. + +## Pending Migrations + +When any migrations are pending the metrics are disabled until the migrations +have been performed. diff --git a/doc/metrics/influxdb_configuration.md b/doc/metrics/influxdb_configuration.md new file mode 100644 index 00000000000..c67c0f15108 --- /dev/null +++ b/doc/metrics/influxdb_configuration.md @@ -0,0 +1,96 @@ +# InfluxDB Configuration + +The default settings provided by InfluxDB are not sufficient for a high traffic +GitLab environment. The settings discussed in this document are based on the +settings GitLab uses for GitLab.com, depending on your own needs you may need to +further adjust them. + +## Requirements + +* InfluxDB 0.9 or newer +* A fairly modern version of Linux +* At least 4GB of RAM +* At least 10GB of storage for InfluxDB data + +Note that the RAM and storage requirements can differ greatly depending on the +amount of data received/stored. To limit the amount of stored data users can +look into [InfluxDB Retention Policies][influxdb-retention]. + +## InfluxDB Server Settings + +Since InfluxDB has many settings that users may wish to customize themselves +(e.g. what port to run InfluxDB on) we'll only cover the essentials. + +### Storage Engine + +InfluxDB comes with different storage engines and as of InfluxDB 0.9 a new +storage engine is available called "tsm1". All users _must_ use the new tsm1 +storage engine (this will be the default engine in upcoming InfluxDB engines). + +### Admin Panel + +Production environments should have the InfluxDB admin panel _disabled_. This +feature can be disabled by adding the following to your InfluxDB configuration +file: + + [admin] + enabled = false + +### HTTP + +HTTP is required when using the InfluxDB CLI or other tools such as Grafana, +thus it should be enabled. When enabling make sure to _also_ enable +authentication: + + [http] + enabled = true + auth-enabled = true + +### UDP + +GitLab writes data to InfluxDB via UDP and thus this must be enabled. Enabling +UDP can be done using the following settings: + + [udp] + enabled = true + bind-address = ":8089" + database = "gitlab" + batch-size = 1000 + batch-pending = 5 + batch-timeout = 1s + read-buffer = 209715200 + +This does the following: + +1. Enable UDP and bind it to port 8089 for all addresses. +2. Store any data received in the "gitlab" database. +3. Define a batch of points to be 1000 points in size and allow a maximum of + 5 batches _or_ flush them automatically after 1 second. +4. Define a UDP read buffer size of 200 MB. + +One of the most important settings here is the UDP read buffer size as if this +value is set too low packets will be dropped. You must also make sure the OS +buffer size is set to the same value, the default value is almost never enough. + +To set the OS buffer size to 200 MB on Linux you can run the following command: + + sysctl -w net.core.rmem_max=209715200 + +To make this permanent, add the following to `/etc/sysctl.conf` and restart the +server: + + net.core.rmem_max=209715200 + +It is **very important** to make sure the buffer sizes are large enough to +handle all data sent to InfluxDB as otherwise you _will_ lose data. The above +buffer sizes are based on the traffic for GitLab.com. Depending on the amount of +traffic users may be able to use a smaller buffer size, but we highly recommend +using _at least_ 100 MB. + +When enabling UDP users should take care to not expose the port to the public as +doing so will allow anybody to write data into your InfluxDB database (as +InfluxDB's UDP protocol doesn't support authentication). We recommend either +whitelisting the allowed IP addresses/ranges, or setting up a VLAN and only +allowing traffic from members of said VLAN. + +[influxdb-retention]: https://docs.influxdata.com/influxdb/v0.9/query_language/database_management/#retention-policy-management diff --git a/doc/metrics/influxdb_schema.md b/doc/metrics/influxdb_schema.md new file mode 100644 index 00000000000..a8e69b5e173 --- /dev/null +++ b/doc/metrics/influxdb_schema.md @@ -0,0 +1,75 @@ +# InfluxDB Schema + +The following measurements are currently stored in InfluxDB: + +* `PROCESS_file_descriptors` +* `PROCESS_gc_statistics` +* `PROCESS_memory_usage` +* `PROCESS_method_calls` +* `PROCESS_object_counts` +* `PROCESS_transactions` +* `PROCESS_views` + +Here `PROCESS` is replaced with either "rails" or "sidekiq" depending on the +process type. In all series any form of duration is stored in milliseconds. + +## PROCESS_file_descriptors + +This measurement contains the number of open file descriptors over time. The +value field `value` contains the number of descriptors. + +## PROCESS_gc_statistics + +This measurement contains Ruby garbage collection statistics such as the amount +of minor/major GC runs (relative to the last sampling interval), the time spent +in garbage collection cycles, and all fields/values returned by `GC.stat`. + +## PROCESS_memory_usage + +This measurement contains the process' memory usage (in bytes) over time. The +value field `value` contains the number of bytes. + +## PROCESS_method_calls + +This measurement contains the methods called during a transaction along with +their durations and a name of the transaction action that invoked the method (if +available). The method call duration is stored in the value field `duration` +while the method name is stored in the tag `method`. The tag `action` contains +the full name of the transaction action. Both the `method` and `action` fields +are in the following format: + + ClassName#method_name + +For example, a method called by the `show` method in the `UsersController` class +would have `action` set to `UsersController#show`. + +## PROCESS_object_counts + +This measurement is used to store retained Ruby objects (per class) and the +amount of retained objects. The number of objects is stored in the `count` value +field while the class name is stored in the `type` tag. + +## PROCESS_transactions + +This measurement is used to store basic transaction details such as the time it +took to complete a transaction, how much time was spent in SQL queries, etc. The +following value fields are available: + +* `duration`: the total duration of the transaction. +* `allocated_memory`: the amount of bytes allocated while the transaction was + running. This value is only reliable when using single-threaded application + servers. +* `method_duration`: the total time spent in method calls. +* `sql_duration`: the total time spent in SQL queries. +* `view_duration`: the total time spent in views. + +## PROCESS_views + +This measurement is used to store view rendering timings for a transaction. The +following value fields are available: + +* `duration`: the rendering time of the view. +* `view`: the path of the view, relative to the application's root directory. + +The `action` tag contains the action name of the transaction that rendered the +view. diff --git a/doc/metrics/introduction.md b/doc/metrics/introduction.md new file mode 100644 index 00000000000..007fff9e7b4 --- /dev/null +++ b/doc/metrics/introduction.md @@ -0,0 +1,55 @@ +# Introduction to GitLab Metrics + +GitLab comes with its own application performance measuring system as of GitLab +8.4, simply called "GitLab Metrics". GitLab Metrics is available in both the +Community and Enterprise editions. + +GitLab Metrics makes it possible to measure a wide variety of statistics +including (but not limited to): + +* The time it took to complete a transaction (a web request or Sidekiq job). +* The time spent in running SQL queries and rendering HAML views. +* The time spent executing (instrumented) Ruby methods. +* Ruby object allocations, and retained objects in particular. +* System statistics such as the process' memory usage and open file descriptors. +* Ruby garbage collection statistics. + +Metrics data is written to [InfluxDB][influxdb] over [UDP](influxdb-udp). Stored +data can be visualized using [Grafana][grafana] or any other application that +supports reading data from InfluxDB. Alternatively data can be queried using the +InfluxDB CLI. + +## Metric Types + +Two types of metrics are collected: + +1. Transaction specific metrics. +2. Sampled metrics, collected at a certain interval in a separate thread. + +### Transaction Metrics + +Transaction metrics are metrics that can be associated with a single +transaction. This includes statistics such as the transaction duration, timings +of any executed SQL queries, time spent rendering HAML views, etc. These metrics +are collected for every Rack request and Sidekiq job processed. + +### Sampled Metrics + +Sampled metrics are metrics that can't be associated with a single transaction. +Examples include garbage collection statistics and retained Ruby objects. These +metrics are collected at a regular interval. This interval is made up out of two +parts: + +1. A user defined interval. +2. A randomly generated offset added on top of the interval, the same offset + can't be used twice in a row. + +The actual interval can be anywhere between a half of the defined interval and a +half above the interval. For example, for a user defined interval of 15 seconds +the actual interval can be anywhere between 7.5 and 22.5. The interval is +re-generated for every sampling run instead of being generated once and re-used +for the duration of the process' lifetime. + +[influxdb]: https://influxdata.com/time-series-platform/influxdb/ +[influxdb-udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/ +[grafana]: http://grafana.org/ -- cgit v1.2.1 From 85e0fce9eee4f20c6c627209828f7351ab0aedae Mon Sep 17 00:00:00 2001 From: Jeroen Nijhof Date: Mon, 18 Jan 2016 17:15:10 +0100 Subject: Add sentry integration --- Gemfile | 3 +++ Gemfile.lock | 3 +++ .../admin/application_settings_controller.rb | 2 ++ app/models/application_setting.rb | 6 ++++++ app/views/admin/application_settings/_form.html.haml | 17 +++++++++++++++++ config/initializers/sentry.rb | 17 +++++++++++++++++ ...20160118155830_add_sentry_to_application_settings.rb | 8 ++++++++ db/schema.rb | 4 +++- spec/models/application_setting_spec.rb | 2 ++ 9 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 config/initializers/sentry.rb create mode 100644 db/migrate/20160118155830_add_sentry_to_application_settings.rb diff --git a/Gemfile b/Gemfile index a9a8bed1064..2363f483fc0 100644 --- a/Gemfile +++ b/Gemfile @@ -314,3 +314,6 @@ gem 'oauth2', '~> 1.0.0' # Soft deletion gem "paranoia", "~> 2.0" + +# Sentry integration +gem 'sentry-raven' diff --git a/Gemfile.lock b/Gemfile.lock index f1bba7f437e..07445f00608 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -725,6 +725,8 @@ GEM activesupport (>= 3.1, < 4.3) select2-rails (3.5.9.3) thor (~> 0.14) + sentry-raven (0.15.3) + faraday (>= 0.7.6) settingslogic (2.0.9) sexp_processor (4.6.0) sham_rack (1.3.6) @@ -1008,6 +1010,7 @@ DEPENDENCIES sdoc (~> 0.3.20) seed-fu (~> 2.3.5) select2-rails (~> 3.5.9) + sentry-raven settingslogic (~> 2.0.9) sham_rack shoulda-matchers (~> 2.8.0) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 91f7d78bd73..9943745208e 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -77,6 +77,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :recaptcha_enabled, :recaptcha_site_key, :recaptcha_private_key, + :sentry_enabled, + :sentry_dsn, restricted_visibility_levels: [], import_sources: [] ) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 6c6c2468374..59563b8823c 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -41,6 +41,8 @@ # recaptcha_site_key :string # recaptcha_private_key :string # metrics_port :integer default(8089) +# sentry_enabled :boolean default(FALSE) +# sentry_dsn :string # class ApplicationSetting < ActiveRecord::Base @@ -82,6 +84,10 @@ class ApplicationSetting < ActiveRecord::Base presence: true, if: :recaptcha_enabled + validates :sentry_dsn, + presence: true, + if: :sentry_enabled + validates_each :restricted_visibility_levels do |record, attr, value| unless value.nil? value.each do |level| diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 83f6814d822..35af5cf620a 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -232,5 +232,22 @@ .col-sm-10 = f.text_field :recaptcha_private_key, class: 'form-control' + %fieldset + %legend Error Reporting and Logging + %p + These settings require a restart to take effect. + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :sentry_enabled do + = f.check_box :sentry_enabled + Enable Sentry + %span.help-block#sentry_help_block Sentry is an error reporting and logging tool + + .form-group + = f.label :sentry_dsn, 'Sentry DSN', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :sentry_dsn, class: 'form-control' + .form-actions = f.submit 'Save', class: 'btn btn-primary' diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb new file mode 100644 index 00000000000..3ef46291981 --- /dev/null +++ b/config/initializers/sentry.rb @@ -0,0 +1,17 @@ +# Be sure to restart your server when you modify this file. + +require 'gitlab/current_settings' +include Gitlab::CurrentSettings + +# allow it to fail: it may do so when create_from_defaults is executed before migrations are actually done +begin + sentry_enabled = current_application_settings.sentry_enabled +rescue + sentry_enabled = false +end + +if sentry_enabled + Raven.configure do |config| + config.dsn = current_application_settings.sentry_dsn + end +end diff --git a/db/migrate/20160118155830_add_sentry_to_application_settings.rb b/db/migrate/20160118155830_add_sentry_to_application_settings.rb new file mode 100644 index 00000000000..fa7ff9d9228 --- /dev/null +++ b/db/migrate/20160118155830_add_sentry_to_application_settings.rb @@ -0,0 +1,8 @@ +class AddSentryToApplicationSettings < ActiveRecord::Migration + def change + change_table :application_settings do |t| + t.boolean :sentry_enabled, default: false + t.string :sentry_dsn + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 9045135dd9a..8394574dfe9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160113111034) do +ActiveRecord::Schema.define(version: 20160118155830) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -62,6 +62,8 @@ ActiveRecord::Schema.define(version: 20160113111034) do t.string "recaptcha_private_key" t.integer "metrics_port", default: 8089 t.integer "metrics_sample_interval", default: 15 + t.boolean "sentry_enabled", default: false + t.string "sentry_dsn" end create_table "audit_events", force: :cascade do |t| diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 91b250265e6..f4c58882757 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -41,6 +41,8 @@ # recaptcha_site_key :string # recaptcha_private_key :string # metrics_port :integer default(8089) +# sentry_enabled :boolean default(FALSE) +# sentry_dsn :string # require 'spec_helper' -- cgit v1.2.1 From 96fb81ee38a3d5e5eb5dc8c95a758c4c853d6c63 Mon Sep 17 00:00:00 2001 From: Ruben Schmidt Date: Mon, 18 Jan 2016 17:23:49 +0000 Subject: Fixed invalid JSON in first JSON Example line 27 --- doc/web_hooks/web_hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index 6420d65cf1b..c29037e89c2 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -56,7 +56,7 @@ X-Gitlab-Event: Push Hook "author": { "name": "Jordi Mallach", "email": "jordi@softcatala.org" - } + }, "added": ["CHANGELOG"], "modified": ["app/controller/application.rb"], "removed": [] -- cgit v1.2.1 From 62571ae1f221477bd982160fb407bb08d7bc0a8e Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Fri, 15 Jan 2016 16:24:16 -0500 Subject: Fixes tooltip doesn't duplicate the "me" name. Checks that "me" is not in the array of names in tooltip --- app/assets/javascripts/awards_handler.coffee | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 9d5ae6c04e9..5ab509b52c1 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -66,14 +66,10 @@ class @AwardsHandler addMeToAuthorList: (emoji) -> award_block = @findEmojiIcon(emoji).parent() - authors = _.compact(award_block.attr("data-original-title").split(", ")) - authors.push("me") - - if authors.length == 1 - award_block.attr("title", "me") - else - award_block.attr("title", authors.join(", ")) - + authors = award_block.attr("data-original-title").split(", ") + if authors.indexOf("me") == -1 + authors.push("me") + award_block.attr("title", authors.join(", ")) @resetTooltip(award_block) resetTooltip: (award) -> -- cgit v1.2.1 From 419bff801be88eb3e2fd1be2e3c6d02514a478bd Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Mon, 18 Jan 2016 11:12:13 -0500 Subject: Removing initial `,`. --- app/assets/javascripts/awards_handler.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 5ab509b52c1..2c9c99d502f 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -66,9 +66,10 @@ class @AwardsHandler addMeToAuthorList: (emoji) -> award_block = @findEmojiIcon(emoji).parent() - authors = award_block.attr("data-original-title").split(", ") + authors = award_block.attr("data-original-title").trim().split(", ") if authors.indexOf("me") == -1 authors.push("me") + console.log('authors'); award_block.attr("title", authors.join(", ")) @resetTooltip(award_block) -- cgit v1.2.1 From c298068c45682ad2b75727029872878cd3cc5522 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Mon, 18 Jan 2016 15:40:48 -0500 Subject: Fix underlying issue with emoji reactions Issue was: blank space was rendering as a element in an array of authors. Element was being used by `join`. Original fix !2450 was trying to remove the space after it happened. This checks properly for it and only moves forward if it does not exist. Also removes "me" upon unchecking emoji. --- app/assets/javascripts/awards_handler.coffee | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 2c9c99d502f..42644229490 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -44,8 +44,7 @@ class @AwardsHandler decrementCounter: (emoji) -> counter = @findEmojiIcon(emoji).siblings(".counter") emojiIcon = counter.parent() - - if parseInt(counter.text()) > 1 + if parseInt(counter.text()) > 0 counter.text(parseInt(counter.text()) - 1) emojiIcon.removeClass("active") @removeMeFromAuthorList(emoji) @@ -60,16 +59,19 @@ class @AwardsHandler removeMeFromAuthorList: (emoji) -> award_block = @findEmojiIcon(emoji).parent() authors = award_block.attr("data-original-title").split(", ") - authors = _.without(authors, "me").join(", ") - award_block.attr("title", authors) + if authors.indexOf("me") != -1 + authors.splice(authors.indexOf("me"),1) + award_block.closest(".award").attr("data-original-title", authors.join(", ")) @resetTooltip(award_block) addMeToAuthorList: (emoji) -> award_block = @findEmojiIcon(emoji).parent() - authors = award_block.attr("data-original-title").trim().split(", ") + origTitle = award_block.attr("data-original-title").trim() + authors = [] + if origTitle + authors = origTitle.split(', ') if authors.indexOf("me") == -1 authors.push("me") - console.log('authors'); award_block.attr("title", authors.join(", ")) @resetTooltip(award_block) -- cgit v1.2.1 From 84354b4bd28eff817f068e48bd64faf882952e96 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 18 Jan 2016 21:49:04 +0100 Subject: Fix typos, grammar and other styleguide issues [ci skip] --- doc/api/README.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/doc/api/README.md b/doc/api/README.md index a9bfd2f7ebc..9f3ad126320 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -1,7 +1,7 @@ # GitLab API Automate GitLab via a simple and powerful API. All definitions can be found -under [/lib/api](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/api). +under [`/lib/api`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/api). ## Resources @@ -36,9 +36,9 @@ following locations: ## Authentication All API requests require authentication. You need to pass a `private_token` -parameter by URL or header. If passed as a header, the header name must be -**PRIVATE-TOKEN** (capital and with dash instead of underscore). You can find -or reset your private token in your account page (`/profile/account`). +parameter via query string or header. If passed as a header, the header name +must be `PRIVATE-TOKEN` (uppercase and with a dash instead of an underscore). +You can find or reset your private token in your account page (`/profile/account`). If `private_token` is invalid or omitted, then an error message will be returned with status code `401`: @@ -107,12 +107,12 @@ The following table shows the possible return codes for API requests. | ------------- | ----------- | | `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. | | `201 Created` | The `POST` request was successful and the resource is returned as JSON. | -| `400 Bad Request` | A required attribute of the API request is missing, e.g. the title of an issue is not given. | +| `400 Bad Request` | A required attribute of the API request is missing, e.g., the title of an issue is not given. | | `401 Unauthorized` | The user is not authenticated, a valid [user token](#authentication) is necessary. | -| `403 Forbidden` | The request is not allowed, e.g. the user is not allowed to delete a project. | -| `404 Not Found` | A resource could not be accessed, e.g. an ID for a resource could not be found. | +| `403 Forbidden` | The request is not allowed, e.g., the user is not allowed to delete a project. | +| `404 Not Found` | A resource could not be accessed, e.g., an ID for a resource could not be found. | | `405 Method Not Allowed` | The request is not supported. | -| `409 Conflict` | A conflicting resource already exists, e.g. creating a project with a name that already exists. | +| `409 Conflict` | A conflicting resource already exists, e.g., creating a project with a name that already exists. | | `422 Unprocessable` | The entity could not be processed. | | `500 Server Error` | While handling the request something went wrong server-side. | @@ -120,9 +120,9 @@ The following table shows the possible return codes for API requests. All API requests support performing an API call as if you were another user, provided your private token is from an administrator account. You need to pass -the `sudo` parameter either by URL or a header with an ID/username of the user -you want to perform the operation as. If passed as a header, the header name -must be **SUDO** (capitals). +the `sudo` parameter either via query string or a header with an ID/username of +the user you want to perform the operation as. If passed as a header, the +header name must be `SUDO` (uppercase). If a non administrative `private_token` is provided, then an error message will be returned with status code `403`: @@ -133,7 +133,7 @@ be returned with status code `403`: } ``` -If the sudo user id or username cannot be found, an error message will be +If the sudo user ID or username cannot be found, an error message will be returned with status code `404`: ```json @@ -168,8 +168,8 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: 23" "https:/ ## Pagination -Sometimes the returned result will span across many lines. When listing -resources you can pass the following parameters. +Sometimes the returned result will span across many pages. When listing +resources you can pass the following parameters: | Parameter | Description | | --------- | ----------- | @@ -230,7 +230,7 @@ Additional pagination headers are also sent back. | `X-Next-Page` | The index of the next page | | `X-Prev-Page` | The index of the previous page | -## id vs iid +## `id` vs `iid` When you work with the API, you may notice two similar fields in API entities: `id` and `iid`. The main difference between them is scope. @@ -257,14 +257,14 @@ GET /projects/42/issues/:iid ## Data validation and error reporting -When working with the API you may encounter validation errors. In such case, +When working with the API you may encounter validation errors, in which case the API will answer with an HTTP `400` status. Such errors appear in two cases: -- A required attribute of the API request is missing, e.g. the title of an +- A required attribute of the API request is missing, e.g., the title of an issue is not given -- An attribute did not pass the validation, e.g. user bio is too long +- An attribute did not pass the validation, e.g., user bio is too long When an attribute is missing, you will get something like: @@ -316,7 +316,7 @@ follows: ## Clients There are many unofficial GitLab API Clients for most of the popular -programming languages. Visit the [GitLab website][] for a complete list. +programming languages. Visit the [GitLab website] for a complete list. [GitLab website]: https://about.gitlab.com/applications/#api-clients "Clients using the GitLab API" [lib-api-url]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/api/api.rb -- cgit v1.2.1 From c426a571dbdffcedee4200f5acc56aae02df3bf9 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Mon, 18 Jan 2016 19:27:59 -0500 Subject: Fix the situation where the user has named themselves "me" Stopped looking for existing user since remove functionality is fixed. --- app/assets/javascripts/awards_handler.coffee | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 42644229490..7136ef3258b 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -59,8 +59,7 @@ class @AwardsHandler removeMeFromAuthorList: (emoji) -> award_block = @findEmojiIcon(emoji).parent() authors = award_block.attr("data-original-title").split(", ") - if authors.indexOf("me") != -1 - authors.splice(authors.indexOf("me"),1) + authors.splice(authors.indexOf("me"),1) award_block.closest(".award").attr("data-original-title", authors.join(", ")) @resetTooltip(award_block) @@ -70,8 +69,7 @@ class @AwardsHandler authors = [] if origTitle authors = origTitle.split(', ') - if authors.indexOf("me") == -1 - authors.push("me") + authors.push("me") award_block.attr("title", authors.join(", ")) @resetTooltip(award_block) -- cgit v1.2.1 From 78754cb1c9ad3dce56dd8257e95fc9241746b9f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Mon, 18 Jan 2016 21:39:23 -0500 Subject: Update Issues/MRs updated_at when user leaves comments. --- app/services/notes/create_service.rb | 4 +++- features/project/issues/issues.feature | 8 ++++++++ features/project/merge_requests.feature | 19 +++++++++++++++++++ features/steps/project/issues/issues.rb | 5 +++++ features/steps/project/merge_requests.rb | 8 ++++++++ features/steps/shared/note.rb | 7 +++++++ 6 files changed, 50 insertions(+), 1 deletion(-) diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index a8486e6a5a1..ef98f0fd223 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -10,7 +10,9 @@ module Notes # Skip system notes, like status changes and cross-references and awards unless note.system || note.is_award - event_service.leave_note(note, note.author) + event = event_service.leave_note(note, note.author) + + note.noteable.touch if event.commented? note.create_cross_references! execute_hooks(note) end diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index ab234bc7507..1502b0952cd 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -51,6 +51,14 @@ Feature: Project Issues Then I should see comment "XML attached" And I should see an error alert section within the comment form + @javascript + Scenario: Visiting Issues after leaving a comment + Given I visit issue page "Release 0.4" + And I leave a comment like "XML attached" + And I visit project "Shop" issues page + And I sort the list by "Last updated" + Then I should see "Release 0.4" at the top + @javascript Scenario: I search issue Given I fill in issue search with "Re" diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index aa9078b878f..f1629a26f10 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -75,6 +75,25 @@ Feature: Project Merge Requests And I leave a comment like "XML attached" Then I should see comment "XML attached" + @javascript + Scenario: Visiting Merge Requests after leaving a comment + Given project "Shop" have "Bug NS-05" open merge request with diffs inside + And I visit merge request page "Bug NS-04" + And I leave a comment like "XML attached" + And I visit project "Shop" merge requests page + And I sort the list by "Last updated" + Then I should see "Bug NS-04" at the top + + @javascript + Scenario: Visiting Merge Requests after commenting on diffs + Given project "Shop" have "Bug NS-05" open merge request with diffs inside + And I visit merge request page "Bug NS-05" + And I click on the Changes tab + And I leave a comment like "Line is wrong" on diff + And I visit project "Shop" merge requests page + And I sort the list by "Last updated" + Then I should see "Bug NS-05" at the top + @javascript Scenario: I comment on a merge request diff Given project "Shop" have "Bug NS-05" open merge request with diffs inside diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index 8e8c9c57452..d556b73f9fd 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -293,6 +293,11 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps expect(page).to have_content('Yay!') end end + + step 'I should see "Release 0.4" at the top' do + expect(page.find('ul.content-list.issues-list li.issue:first-child')).to have_content("Release 0.4") + end + def filter_issue(text) fill_in 'issue_search', with: text end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index be993d11093..bec9920f91b 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -415,6 +415,14 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end end + step 'I should see "Bug NS-05" at the top' do + expect(page.find('ul.content-list.mr-list li.merge-request:first-child')).to have_content("Bug NS-05") + end + + step 'I should see "Bug NS-04" at the top' do + expect(page.find('ul.content-list.mr-list li.merge-request:first-child')).to have_content("Bug NS-04") + end + def merge_request @merge_request ||= MergeRequest.find_by!(title: "Bug NS-05") end diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index 444d6726f99..eb6df61b8e6 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -144,4 +144,11 @@ module SharedNote expect(page).to have_content("+1 Awesome!") end end + + step 'I sort the list by "Last updated"' do + find('button.dropdown-toggle.btn').click + page.within('ul.dropdown-menu.dropdown-menu-align-right li') do + click_link "Last updated" + end + end end -- cgit v1.2.1 From 08c482b87ac08dbea533b931fabfb6939edc45ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Mon, 18 Jan 2016 21:59:22 -0500 Subject: Check if object respond to #touch before update. --- app/services/notes/create_service.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index ef98f0fd223..b39ebac1092 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -11,8 +11,9 @@ module Notes # Skip system notes, like status changes and cross-references and awards unless note.system || note.is_award event = event_service.leave_note(note, note.author) + noteable = note.noteable - note.noteable.touch if event.commented? + noteable.touch if event.commented? && noteable.respond_to?(:touch) note.create_cross_references! execute_hooks(note) end -- cgit v1.2.1 From 1ede975cde6d32a74b2d4d523e5c0e242c89d50d Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 19 Jan 2016 11:18:17 +0100 Subject: Styleguide the commit status API [ci skip] --- doc/api/commits.md | 202 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 127 insertions(+), 75 deletions(-) diff --git a/doc/api/commits.md b/doc/api/commits.md index a07c9dff54e..4008629e0a2 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -158,8 +158,21 @@ Example response: ## Post comment to commit -Adds a comment to a commit. Optionally you can post comments on a specific line -of a commit. In that case `path`, `line` and `line_type` are required. +Adds a comment to a commit. + +In order to post a comment in a particular line of a particular file, you must +specify the full commit SHA, the `path`, the `line` and `line_type` should be +`new`. + +The comment will be added at the end of the last commit if at least one of the +cases below is valid: + +- the `sha` is instead a branch or a tag and the `line` or `path` are invalid +- the `line` number is invalid (does not exist) +- the `path` is invalid (does not exist) + +In any of the above cases, the response of `line`, `line_type` and `path` is +set to `null`. ``` POST /projects/:id/repository/commits/:sha/comments @@ -168,35 +181,33 @@ POST /projects/:id/repository/commits/:sha/comments | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer | yes | The ID of a project | -| `sha` | string | yes | The commit hash or name of a repository branch or tag | -| `note` | string | yes | Text of comment | +| `sha` | string | yes | The commit SHA or name of a repository branch or tag | +| `note` | string | yes | The text of the comment | | `path` | string | no | The file path relative to the repository | -| `line` | integer | no | The line number | +| `line` | integer | no | The line number where the comment should be placed | | `line_type` | string | no | The line type. Takes `new` or `old` as arguments | -In order to post a comment in a particular line of a particular file, you must -specify `path`, `line` and `line_type` should be `new`. - ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/comments?note=New%20comment&path=CHANGELOG&line=664&line_type=new" +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -F "note=Nice picture man\!" -F "path=dudeism.md" -F "line=11" -F "line_type=new" https://gitlab.example.com/api/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/comments ``` Example response: ```json { - "author": { - "id": 1, - "username": "admin", - "email": "admin@local.host", - "name": "Administrator", - "blocked": false, - "created_at": "2012-04-29T08:46:00Z" - }, - "note": "text1", - "path": "example.rb", - "line": 5, - "line_type": "new" + "author" : { + "web_url" : "https://gitlab.example.com/u/thedude", + "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png", + "username" : "thedude", + "state" : "active", + "name" : "Jeff Lebowski", + "id" : 28 + }, + "created_at" : "2016-01-19T09:44:55.600Z", + "line_type" : "new", + "path" : "dudeism.md", + "line" : 11, + "note" : "Nice picture man!" } ``` @@ -208,75 +219,116 @@ Get the statuses of a commit in a project. GET /projects/:id/repository/commits/:sha/statuses ``` -Parameters: +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project +| `sha` | string | yes | The commit SHA +| `ref_name`| string | no | The name of a repository branch or tag or, if not given, the default branch +| `stage` | string | no | Filter by [build stage](../ci/yaml/README.md#stages), e.g., `test` +| `name` | string | no | Filter by [job name](../ci/yaml/README.md#jobs), e.g., `bundler:audit` +| `all` | boolean | no | Return all statuses, not only the latest ones -- `id` (required) - The ID of a project -- `sha` (required) - The commit SHA -- `ref` (optional) - Filter by ref name, it can be branch or tag -- `stage` (optional) - Filter by stage -- `name` (optional) - Filer by status name, eg. jenkins -- `all` (optional) - The flag to return all statuses, not only latest ones +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/statuses +``` + +Example response: ```json [ - { - "id": 13, - "sha": "b0b3a907f41409829b307a28b82fdbd552ee5a27", - "ref": "test", - "status": "success", - "name": "ci/jenkins", - "target_url": "http://jenkins/project/url", - "description": "Jenkins success", - "created_at": "2015-10-12T09:47:16.250Z", - "started_at": "2015-10-12T09:47:16.250Z", - "finished_at": "2015-10-12T09:47:16.262Z", - "author": { - "id": 1, - "username": "admin", - "email": "admin@local.host", - "name": "Administrator", - "blocked": false, - "created_at": "2012-04-29T08:46:00Z" - } - } + ... + + { + "status" : "pending", + "created_at" : "2016-01-19T08:40:25.934Z", + "started_at" : null, + "name" : "bundler:audit", + "allow_failure" : true, + "author" : { + "username" : "thedude", + "state" : "active", + "web_url" : "https://gitlab.example.com/u/thedude", + "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png", + "id" : 28, + "name" : "Jeff Lebowski" + }, + "description" : null, + "sha" : "18f3e63d05582537db6d183d9d557be09e1f90c8", + "target_url" : "https://gitlab.example.com/thedude/gitlab-ce/builds/91", + "finished_at" : null, + "id" : 91, + "ref" : "master" + }, + { + "started_at" : null, + "name" : "flay", + "allow_failure" : false, + "status" : "pending", + "created_at" : "2016-01-19T08:40:25.832Z", + "target_url" : "https://gitlab.example.com/thedude/gitlab-ce/builds/90", + "id" : 90, + "finished_at" : null, + "ref" : "master", + "sha" : "18f3e63d05582537db6d183d9d557be09e1f90c8", + "author" : { + "id" : 28, + "name" : "Jeff Lebowski", + "username" : "thedude", + "web_url" : "https://gitlab.example.com/u/thedude", + "state" : "active", + "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png" + }, + "description" : null + }, + + ... ] ``` -## Post the status to commit +## Post the build status to a commit -Adds or updates a status of a commit. +Adds or updates a build status of a commit. ``` POST /projects/:id/statuses/:sha ``` -- `id` (required) - The ID of a project -- `sha` (required) - The commit SHA -- `state` (required) - The state of the status. Can be: pending, running, success, failed, canceled -- `ref` (optional) - The ref (branch or tag) to which the status refers -- `name` or `context` (optional) - The label to differentiate this status from the status of other systems. Default: "default" -- `target_url` (optional) - The target URL to associate with this status -- `description` (optional) - The short description of the status +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project +| `sha` | string | yes | The commit SHA +| `state` | string | yes | The state of the status. Can be one of the following: `pending`, `running`, `success`, `failed`, `canceled` +| `ref` | string | no | The `ref` (branch or tag) to which the status refers +| `name` or `context` | string | no | The label to differentiate this status from the status of other systems. Default value is `default` +| `target_url` | string | no | The target URL to associate with this status +| `description` | string | no | The short description of the status + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/17/statuses/18f3e63d05582537db6d183d9d557be09e1f90c8?state=success" +``` + +Example response: ```json { - "id": 13, - "sha": "b0b3a907f41409829b307a28b82fdbd552ee5a27", - "ref": "test", - "status": "success", - "name": "ci/jenkins", - "target_url": "http://jenkins/project/url", - "description": "Jenkins success", - "created_at": "2015-10-12T09:47:16.250Z", - "started_at": "2015-10-12T09:47:16.250Z", - "finished_at": "2015-10-12T09:47:16.262Z", - "author": { - "id": 1, - "username": "admin", - "email": "admin@local.host", - "name": "Administrator", - "blocked": false, - "created_at": "2012-04-29T08:46:00Z" - } + "author" : { + "web_url" : "https://gitlab.example.com/u/thedude", + "name" : "Jeff Lebowski", + "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png", + "username" : "thedude", + "state" : "active", + "id" : 28 + }, + "name" : "default", + "sha" : "18f3e63d05582537db6d183d9d557be09e1f90c8", + "status" : "success", + "description" : null, + "id" : 93, + "target_url" : null, + "ref" : null, + "started_at" : null, + "created_at" : "2016-01-19T09:05:50.355Z", + "allow_failure" : false, + "finished_at" : "2016-01-19T09:05:50.365Z" } ``` -- cgit v1.2.1 From 0a8039eb7790426880bdd7b9d67775aeb6e5dac7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 19 Jan 2016 13:56:36 +0100 Subject: Remove useless assignments --- lib/gitlab/diff/inline_diff_marker.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb index 405465a641f..8998ccba5ce 100644 --- a/lib/gitlab/diff/inline_diff_marker.rb +++ b/lib/gitlab/diff/inline_diff_marker.rb @@ -31,10 +31,8 @@ module Gitlab def position_mapping @position_mapping ||= begin mapping = [] - raw_pos = 0 rich_pos = 0 (0..raw_line.length).each do |raw_pos| - raw_char = raw_line[raw_pos] rich_char = rich_line[rich_pos] # The raw and rich lines are the same except for HTML tags, -- cgit v1.2.1 From 512bebe21d7f57b691a1c8355581feb64b9b6292 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 19 Jan 2016 14:52:41 +0100 Subject: Refactor Gitlab::Highlight and fix tests --- app/helpers/blob_helper.rb | 8 ++- app/views/projects/blame/show.html.haml | 5 +- app/views/shared/_file_highlight.html.haml | 3 +- lib/gitlab/highlight.rb | 26 ++++++-- spec/fixtures/parallel_diff_result.yml | 98 +++++++++++++++--------------- spec/helpers/blob_helper_spec.rb | 63 ++++++++++--------- spec/lib/gitlab/diff/highlight_spec.rb | 63 ++++++------------- 7 files changed, 131 insertions(+), 135 deletions(-) diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index be856242c43..7c55934edda 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -1,6 +1,10 @@ module BlobHelper - def highlight(blob_name, blob_content, nowrap: false, continue: false) - Gitlab::Highlight.highlight(blob_name, blob_content, nowrap: nowrap, continue: continue) + def highlighter(blob_name, blob_content, nowrap: false) + Gitlab::Highlight.new(blob_name, blob_content, nowrap: nowrap) + end + + def highlight(blob_name, blob_content, nowrap: false) + Gitlab::Highlight.highlight(blob_name, blob_content, nowrap: nowrap) end def no_highlight_files diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 8d9ec068a43..d5d04954490 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -15,6 +15,7 @@ .file-content.blame.highlight %table - current_line = 1 + - blame_highlighter = highlighter(@blob.name, @blob.data, nowrap: true) - @blame.each do |blame_group| %tr %td.blame-commit @@ -41,5 +42,5 @@ %pre{class: 'code highlight white'} %code - blame_group[:lines].each do |line| - :erb - <%= highlight(@blob.name, line, nowrap: true, continue: true).html_safe %> + :preserve + #{blame_highlighter.highlight(line)} diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml index 2bc98983d67..1bef1de433a 100644 --- a/app/views/shared/_file_highlight.html.haml +++ b/app/views/shared/_file_highlight.html.haml @@ -9,5 +9,4 @@ %i.fa.fa-link = i .blob-content{data: {blob_id: blob.id}} - :preserve - #{highlight(blob.name, blob.data)} + = highlight(blob.name, blob.data) diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index a5b041687e3..28cfebef968 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -1,10 +1,7 @@ module Gitlab class Highlight - def self.highlight(blob_name, blob_content, nowrap: true, continue: false) - formatter = rouge_formatter(nowrap: nowrap) - - lexer = Rouge::Lexer.guess(filename: blob_name, source: blob_content).new rescue Rouge::Lexers::PlainText - formatter.format(lexer.lex(blob_content, continue: continue)).html_safe + def self.highlight(blob_name, blob_content, nowrap: true) + new(blob_name, blob_content, nowrap: nowrap).highlight(blob_content, continue: false) end def self.highlight_lines(repository, ref, file_name) @@ -14,9 +11,26 @@ module Gitlab highlight(file_name, blob.data).lines.map!(&:html_safe) end + def initialize(blob_name, blob_content, nowrap: true) + @formatter = rouge_formatter(nowrap: nowrap) + @lexer = Rouge::Lexer.guess(filename: blob_name, source: blob_content).new rescue Rouge::Lexers::PlainText + end + + def highlight(text, continue: true) + @formatter.format(lex(text, continue: continue)).html_safe + end + private - def self.rouge_formatter(options = {}) + def lex(text, continue: true) + if @lexer == Rouge::Lexers::PlainText + @lexer.lex(text) + else + @lexer.lex(text, continue: continue) + end + end + + def rouge_formatter(options = {}) options = options.reverse_merge( nowrap: true, cssclass: 'code highlight', diff --git a/spec/fixtures/parallel_diff_result.yml b/spec/fixtures/parallel_diff_result.yml index 4583de1231e..0fb9bf6c5d8 100644 --- a/spec/fixtures/parallel_diff_result.yml +++ b/spec/fixtures/parallel_diff_result.yml @@ -10,37 +10,37 @@ :text: "@@ -6,12 +6,18 @@ module Popen" :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 - :left: - :type: + :type: :number: 6 :text: |2 :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 :right: - :type: + :type: :number: 6 :text: |2 :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 - :left: - :type: + :type: :number: 7 :text: |2 def popen(cmd, path=nil) :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7 :right: - :type: + :type: :number: 7 :text: |2 def popen(cmd, path=nil) :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7 - :left: - :type: + :type: :number: 8 :text: |2 unless cmd.is_a?(Array) :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8 :right: - :type: + :type: :number: 8 :text: |2 unless cmd.is_a?(Array) @@ -55,40 +55,40 @@ :type: new :number: 9 :text: | - + raise RuntimeError, "System commands must be given as an array of strings" + + raise RuntimeError, "System commands must be given as an array of strings" :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9 - :left: - :type: + :type: :number: 10 :text: |2 end :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10 :right: - :type: + :type: :number: 10 :text: |2 end :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10 - :left: - :type: + :type: :number: 11 :text: |2 :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11 :right: - :type: + :type: :number: 11 :text: |2 :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11 - :left: - :type: + :type: :number: 12 :text: |2 path ||= Dir.pwd :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12 :right: - :type: + :type: :number: 12 :text: |2 path ||= Dir.pwd @@ -101,9 +101,9 @@ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_13_13 :right: :type: old - :number: + :number: :text: '' - :line_code: + :line_code: - :left: :type: old :number: 14 @@ -117,8 +117,8 @@ + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_13 - :left: - :type: - :number: + :type: + :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14 :right: @@ -128,8 +128,8 @@ + vars = { :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14 - :left: - :type: - :number: + :type: + :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15 :right: @@ -139,8 +139,8 @@ + "PWD" => path :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15 - :left: - :type: - :number: + :type: + :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16 :right: @@ -150,8 +150,8 @@ + } :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16 - :left: - :type: - :number: + :type: + :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17 :right: @@ -161,8 +161,8 @@ + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17 - :left: - :type: - :number: + :type: + :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18 :right: @@ -172,8 +172,8 @@ + options = { :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18 - :left: - :type: - :number: + :type: + :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19 :right: @@ -183,8 +183,8 @@ + chdir: path :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19 - :left: - :type: - :number: + :type: + :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20 :right: @@ -194,37 +194,37 @@ + } :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20 - :left: - :type: + :type: :number: 15 :text: |2 :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21 :right: - :type: + :type: :number: 21 :text: |2 :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21 - :left: - :type: + :type: :number: 16 :text: |2 unless File.directory?(path) :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22 :right: - :type: + :type: :number: 22 :text: |2 unless File.directory?(path) :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22 - :left: - :type: + :type: :number: 17 :text: |2 FileUtils.mkdir_p(path) :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23 :right: - :type: + :type: :number: 23 :text: |2 FileUtils.mkdir_p(path) @@ -240,44 +240,44 @@ :text: "@@ -19,6 +25,7 @@ module Popen" :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 - :left: - :type: + :type: :number: 19 :text: |2 :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 :right: - :type: + :type: :number: 25 :text: |2 :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 - :left: - :type: + :type: :number: 20 :text: |2 @cmd_output = "" :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26 :right: - :type: + :type: :number: 26 :text: |2 @cmd_output = "" :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26 - :left: - :type: + :type: :number: 21 :text: |2 @cmd_status = 0 :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27 :right: - :type: + :type: :number: 27 :text: |2 @cmd_status = 0 :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27 - :left: - :type: - :number: + :type: + :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28 :right: @@ -287,37 +287,37 @@ + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28 - :left: - :type: + :type: :number: 22 :text: |2 Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29 :right: - :type: + :type: :number: 29 :text: |2 Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29 - :left: - :type: + :type: :number: 23 :text: |2 @cmd_output << stdout.read :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30 :right: - :type: + :type: :number: 30 :text: |2 @cmd_output << stdout.read :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30 - :left: - :type: + :type: :number: 24 :text: |2 @cmd_output << stderr.read :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31 :right: - :type: + :type: :number: 31 :text: |2 @cmd_output << stderr.read diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index b8bba36439a..87849230dbe 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -1,22 +1,22 @@ require 'spec_helper' describe BlobHelper do - describe 'highlight' do - let(:blob_name) { 'test.lisp' } - let(:no_context_content) { ":type \"assem\"))" } - let(:blob_content) { "(make-pathname :defaults name\n#{no_context_content}" } - let(:split_content) { blob_content.split("\n") } - let(:multiline_content) do - %q( - def test(input): - """This is line 1 of a multi-line comment. - This is line 2. - """ - ) - end + let(:blob_name) { 'test.lisp' } + let(:no_context_content) { ":type \"assem\"))" } + let(:blob_content) { "(make-pathname :defaults name\n#{no_context_content}" } + let(:split_content) { blob_content.split("\n") } + let(:multiline_content) do + %q( + def test(input): + """This is line 1 of a multi-line comment. + This is line 2. + """ + ) + end + describe '#highlight' do it 'should return plaintext for unknown lexer context' do - result = highlight(blob_name, no_context_content, nowrap: true, continue: false) + result = helper.highlight(blob_name, no_context_content, nowrap: true) expect(result).to eq(':type "assem"))') end @@ -24,28 +24,17 @@ describe BlobHelper do expected = %Q[(make-pathname :defaults name :type "assem"))] - expect(highlight(blob_name, blob_content, nowrap: true, continue: false)).to eq(expected) - end - - it 'should highlight continued blocks' do - # Both lines have LC1 as ID since formatter doesn't support continue at the moment - expected = [ - '(make-pathname :defaults name', - ':type "assem"))' - ] - - result = split_content.map{ |content| highlight(blob_name, content, nowrap: true, continue: true) } - expect(result).to eq(expected) + expect(helper.highlight(blob_name, blob_content, nowrap: true)).to eq(expected) end it 'should highlight multi-line comments' do - result = highlight(blob_name, multiline_content, nowrap: true, continue: false) + result = helper.highlight(blob_name, multiline_content, nowrap: true) html = Nokogiri::HTML(result) lines = html.search('.s') expect(lines.count).to eq(3) expect(lines[0].text).to eq('"""This is line 1 of a multi-line comment.') - expect(lines[1].text).to eq(' This is line 2.') - expect(lines[2].text).to eq(' """') + expect(lines[1].text).to eq(' This is line 2.') + expect(lines[2].text).to eq(' """') end context 'diff highlighting' do @@ -59,9 +48,23 @@ describe BlobHelper do end it 'should highlight each line properly' do - result = highlight(blob_name, blob_content, nowrap: true, continue: false) + result = helper.highlight(blob_name, blob_content, nowrap: true) expect(result).to eq(expected) end end end + + describe "#highlighter" do + it 'should highlight continued blocks' do + # Both lines have LC1 as ID since formatter doesn't support continue at the moment + expected = [ + '(make-pathname :defaults name', + ':type "assem"))' + ] + + highlighter = helper.highlighter(blob_name, blob_content, nowrap: true) + result = split_content.map{ |content| highlighter.highlight(content) } + expect(result).to eq(expected) + end + end end diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index f307dcaae44..b54e95483d3 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -8,59 +8,34 @@ describe Gitlab::Diff::Highlight, lib: true do let(:diff) { commit.diffs.first } let(:diff_file) { Gitlab::Diff::File.new(diff, [commit.parent, commit]) } - describe '.process_diff_lines' do - context 'when processing Gitlab::Diff::Line objects' do - let(:diff_lines) { Gitlab::Diff::Highlight.process_diff_lines(diff_file) } + describe '#highlight' do + let(:diff_lines) { Gitlab::Diff::Highlight.new(diff_file).highlight } - it 'should return Gitlab::Diff::Line elements' do - expect(diff_lines.first).to be_an_instance_of(Gitlab::Diff::Line) - end - - it 'should highlight the code' do - code = %Q{ def popen(cmd, path=nil)\n} - - expect(diff_lines[2].text).to eq(code) - end - - it 'should not generate the inline diff markup' do - expect(diff_lines[5].text).not_to match(Regexp.new(Regexp.escape(''))) - end - - it 'should not modify "match" lines' do - expect(diff_lines[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') - expect(diff_lines[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') - end - - it 'should highlight unchanged lines' do - code = %Q{ def popen(cmd, path=nil)\n} + it 'should return Gitlab::Diff::Line elements' do + expect(diff_lines.first).to be_an_instance_of(Gitlab::Diff::Line) + end - expect(diff_lines[2].text).to eq(code) - end + it 'should not modify "match" lines' do + expect(diff_lines[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') + expect(diff_lines[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') + end - it 'should highlight added lines' do - code = %Q{+ raise RuntimeError, "System commands must be given as an array of strings"\n} + it 'should highlight unchanged lines' do + code = %Q{ def popen(cmd, path=nil)\n} - expect(diff_lines[5].text).to eq(code) - end + expect(diff_lines[2].text).to eq(code) + end - it 'should highlight removed lines' do - code = %Q{- raise "System commands must be given as an array of strings"\n} + it 'should highlight removed lines' do + code = %Q{- raise "System commands must be given as an array of strings"\n} - expect(diff_lines[4].text).to eq(code) - end + expect(diff_lines[4].text).to eq(code) end - end - describe '.highlight_lines' do - let(:lines) do - Gitlab::Diff::Highlight.highlight_lines(project.repository, commit.id, 'files/ruby/popen.rb') - end + it 'should highlight added lines' do + code = %Q{+ raise RuntimeError, "System commands must be given as an array of strings"\n} - it 'should properly highlight all the lines' do - expect(lines[4]).to eq(%Q{ extend self\n}) - expect(lines[21]).to eq(%Q{ unless File.directory?(path)\n}) - expect(lines[26]).to eq(%Q{ @cmd_status = 0\n}) + expect(diff_lines[5].text).to eq(code) end end - end -- cgit v1.2.1 From 5c7259c7c30228bf85a4efbbddb673046e8d733f Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 19 Jan 2016 15:13:37 +0100 Subject: Don't crash when file can't be highlighted --- lib/gitlab/highlight.rb | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index 28cfebef968..4ddb4fea977 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -17,19 +17,13 @@ module Gitlab end def highlight(text, continue: true) - @formatter.format(lex(text, continue: continue)).html_safe + @formatter.format(@lexer.lex(text, continue: continue)).html_safe + rescue + @formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe end private - def lex(text, continue: true) - if @lexer == Rouge::Lexers::PlainText - @lexer.lex(text) - else - @lexer.lex(text, continue: continue) - end - end - def rouge_formatter(options = {}) options = options.reverse_merge( nowrap: true, -- cgit v1.2.1 From d1938fae3d9a6f4d31576461fae3efb4f09686c4 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 19 Jan 2016 16:26:44 +0100 Subject: Add missing specs --- spec/lib/gitlab/highlight_spec.rb | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 spec/lib/gitlab/highlight_spec.rb diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb new file mode 100644 index 00000000000..1620eb6c60a --- /dev/null +++ b/spec/lib/gitlab/highlight_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Highlight, lib: true do + include RepoHelpers + + let(:project) { create(:project) } + let(:commit) { project.commit(sample_commit.id) } + + describe '.highlight_lines' do + let(:lines) do + Gitlab::Highlight.highlight_lines(project.repository, commit.id, 'files/ruby/popen.rb') + end + + it 'should properly highlight all the lines' do + expect(lines[4]).to eq(%Q{ extend self\n}) + expect(lines[21]).to eq(%Q{ unless File.directory?(path)\n}) + expect(lines[26]).to eq(%Q{ @cmd_status = 0\n}) + end + end + +end -- cgit v1.2.1 From c7264d2a76abdc9d173e2160e27974d41380e93f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 19 Jan 2016 11:40:43 -0500 Subject: Check if MR is not broken. --- app/models/merge_request.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index a9fc6bc167a..9511521879b 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -254,7 +254,7 @@ class MergeRequest < ActiveRecord::Base end def mergeable? - return false unless open? && !work_in_progress? + return false unless open? && !work_in_progress? && !broken? check_if_can_be_merged -- cgit v1.2.1 From 491c2248c05ee9e93eff34f0126802add0e05565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 19 Jan 2016 18:17:56 +0100 Subject: Fix diff comments loaded by AJAX to load comment with diff in discussion tab This commits also fixes two minor issues: - Ensure notes that the current user is not allowed to see are not returned in the AJAX notes loading - Ensure the notes counter badge is decremented of 1 instead of 2 --- CHANGELOG | 1 + app/assets/javascripts/notes.js.coffee | 68 ++++++++++++++++++---------- app/controllers/projects/notes_controller.rb | 22 ++++----- features/project/merge_requests.feature | 11 +++++ features/steps/project/merge_requests.rb | 31 +++++++++++++ 5 files changed, 96 insertions(+), 37 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index adda5373c3c..1fbabd1b77b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.5.0 (unreleased) v 8.4.0 (unreleased) + - Fix diff comments loaded by AJAX to load comment with diff in discussion tab - Ensure Gravatar host looks like an actual host - Consider re-assign as a mention from a notification point of view - Add pagination headers to already paginated API resources diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 2bfc5cb2d9c..3927868cdd4 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -15,6 +15,8 @@ class @Notes @last_fetched_at = last_fetched_at @view = view @noteable_url = document.URL + @notesCountBadge ||= $(".issuable-details").find(".notes-tab .badge") + @initRefresh() @setupMainTargetNoteForm() @cleanBinding() @@ -89,7 +91,7 @@ class @Notes , 15000 refresh: -> - unless document.hidden or (@noteable_url != document.URL) + if not document.hidden and document.URL.indexOf(@noteable_url) is 0 @getContent() getContent: -> @@ -101,7 +103,10 @@ class @Notes notes = data.notes @last_fetched_at = data.last_fetched_at $.each notes, (i, note) => - @renderNote(note) + if note.discussion_with_diff_html? + @renderDiscussionNote(note) + else + @renderNote(note) ### @@ -116,18 +121,21 @@ class @Notes flash.pinTo('.header-content') return + if note.award + awards_handler.addAwardToEmojiBar(note.note) + awards_handler.scrollToAwards() + # render note if it not present in loaded list # or skip if rendered - if @isNewNote(note) && !note.award + else if @isNewNote(note) @note_ids.push(note.id) + $('ul.main-notes-list'). append(note.html). syntaxHighlight() @initTaskList() + @incrementNotesCount() - if note.award - awards_handler.addAwardToEmojiBar(note.note) - awards_handler.scrollToAwards() ### Check if note does not exists on page @@ -144,6 +152,8 @@ class @Notes Note: for rendering inline notes use renderDiscussionNote ### renderDiscussionNote: (note) -> + return unless @isNewNote(note) + @note_ids.push(note.id) form = $("form[rel='" + note.discussion_id + "']") row = form.closest("tr") @@ -151,27 +161,30 @@ class @Notes note_html.syntaxHighlight() # is this the first note of discussion? - if row.is(".js-temp-notes-holder") + discussionContainer = $(".notes[rel='" + note.discussion_id + "']") + if discussionContainer.length is 0 # insert the note and the reply button after the temp row row.after note.discussion_html # remove the note (will be added again below) row.next().find(".note").remove() + # Before that, the container didn't exist + discussionContainer = $(".notes[rel='" + note.discussion_id + "']") + # Add note to 'Changes' page discussions - $(".notes[rel='" + note.discussion_id + "']").append note_html + discussionContainer.append note_html # Init discussion on 'Discussion' page if it is merge request page - if $('body').attr('data-page').indexOf('projects:merge_request') == 0 - discussion_html = $(note.discussion_with_diff_html) - discussion_html.syntaxHighlight() - $('ul.main-notes-list').append(discussion_html) + if $('body').attr('data-page').indexOf('projects:merge_request') is 0 + $('ul.main-notes-list'). + append(note.discussion_with_diff_html). + syntaxHighlight() else # append new note to all matching discussions - $(".notes[rel='" + note.discussion_id + "']").append note_html + discussionContainer.append note_html - # cleanup after successfully creating a diff/discussion note - @removeDiscussionNoteForm(form) + @incrementNotesCount() ### Called in response the main target form has been successfully submitted. @@ -278,6 +291,9 @@ class @Notes addDiscussionNote: (xhr, note, status) => @renderDiscussionNote(note) + # cleanup after successfully creating a diff/discussion note + @removeDiscussionNoteForm($("form[rel='" + note.discussion_id + "']")) + ### Called in response to the edit note form being submitted @@ -349,14 +365,12 @@ class @Notes Removes the actual note from view. Removes the whole discussion if the last note is being removed. ### - removeNote: -> - note = $(this).closest(".note") - note_id = note.attr('id') + removeNote: (e) => + noteId = $(e.currentTarget).closest(".note").attr("id") - $('.note[id="' + note_id + '"]').each -> - note = $(this) + $('.note[id="' + noteId + '"]').each (i, el) => + note = $(el) notes = note.closest(".notes") - count = notes.closest(".issuable-details").find(".notes-tab .badge") # check if this is the last note for this line if notes.find(".note").length is 1 @@ -367,12 +381,10 @@ class @Notes # for diff lines notes.closest("tr").remove() - # update notes count - oldNum = parseInt(count.text()) - count.text(oldNum - 1) - note.remove() + @decrementNotesCount() + ### Called in response to clicking the delete attachment link @@ -542,3 +554,9 @@ class @Notes updateTaskList: -> $('form', this).submit() + + incrementNotesCount: (incrementStep = 1) -> + @notesCountBadge.text parseInt(@notesCountBadge.text()) + incrementStep + + decrementNotesCount: -> + @incrementNotesCount(-1) diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 6f1e186d408..4a2599dda37 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -11,11 +11,9 @@ class Projects::NotesController < Projects::ApplicationController notes_json = { notes: [], last_fetched_at: current_fetched_at } @notes.each do |note| - notes_json[:notes] << { - id: note.id, - html: note_to_html(note), - valid: note.valid? - } + next if note.cross_reference_not_visible_for?(current_user) + + notes_json[:notes] << note_json(note) end render json: notes_json @@ -25,7 +23,7 @@ class Projects::NotesController < Projects::ApplicationController @note = Notes::CreateService.new(project, current_user, note_params).execute respond_to do |format| - format.json { render_note_json(@note) } + format.json { render json: note_json(@note) } format.html { redirect_back_or_default } end end @@ -34,7 +32,7 @@ class Projects::NotesController < Projects::ApplicationController @note = Notes::UpdateService.new(project, current_user, note_params).execute(note) respond_to do |format| - format.json { render_note_json(@note) } + format.json { render json: note_json(@note) } format.html { redirect_back_or_default } end end @@ -99,6 +97,8 @@ class Projects::NotesController < Projects::ApplicationController end def note_to_discussion_html(note) + return unless note.for_diff_line? + if params[:view] == 'parallel' template = "projects/notes/_diff_notes_with_reply_parallel" locals = @@ -131,9 +131,9 @@ class Projects::NotesController < Projects::ApplicationController ) end - def render_note_json(note) + def note_json(note) if note.valid? - render json: { + { valid: true, id: note.id, discussion_id: note.discussion_id, @@ -144,7 +144,7 @@ class Projects::NotesController < Projects::ApplicationController discussion_with_diff_html: note_to_discussion_with_diff_html(note) } else - render json: { + { valid: false, award: note.is_award, errors: note.errors @@ -163,8 +163,6 @@ class Projects::NotesController < Projects::ApplicationController ) end - private - def find_current_user_notes @notes = NotesFinder.new.execute(project, current_user, params) end diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index aa9078b878f..1be5ddcd7e4 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -83,6 +83,15 @@ Feature: Project Merge Requests And I leave a comment like "Line is wrong" on diff And I switch to the merge request's comments tab Then I should see a discussion has started on diff + And I should see a badge of "1" next to the discussion link + + @javascript + Scenario: I see a new comment on merge request diff from another user in the discussion tab + Given project "Shop" have "Bug NS-05" open merge request with diffs inside + And I visit merge request page "Bug NS-05" + And user "John Doe" leaves a comment like "Line is wrong" on diff + Then I should see a discussion by user "John Doe" has started on diff + And I should see a badge of "1" next to the discussion link @javascript Scenario: I edit a comment on a merge request diff @@ -100,9 +109,11 @@ Feature: Project Merge Requests And I visit merge request page "Bug NS-05" And I click on the Changes tab And I leave a comment like "Line is wrong" on diff + And I should see a badge of "1" next to the discussion link And I delete the comment "Line is wrong" on diff And I click on the Discussion tab Then I should not see any discussion + And I should see a badge of "0" next to the discussion link @javascript Scenario: I comment on a line of a commit in merge request diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index be993d11093..c0b9984997c 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -181,6 +181,15 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps leave_comment "Line is wrong" end + step 'user "John Doe" leaves a comment like "Line is wrong" on diff' do + mr = MergeRequest.find_by(title: "Bug NS-05") + create(:note_on_merge_request_diff, project: project, + noteable_id: mr.id, + author: user_exists("John Doe"), + line_code: sample_commit.line_code, + note: 'Line is wrong') + end + step 'I leave a comment like "Line is wrong" on diff in commit' do click_diff_line(sample_commit.line_code) leave_comment "Line is wrong" @@ -238,6 +247,22 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end end + step 'I should see a discussion by user "John Doe" has started on diff' do + page.within(".notes .discussion") do + page.should have_content "#{user_exists("John Doe").name} started a discussion" + page.should have_content sample_commit.line_code_path + page.should have_content "Line is wrong" + end + end + + step 'I should see a badge of "1" next to the discussion link' do + expect_discussion_badge_to_have_counter("1") + end + + step 'I should see a badge of "0" next to the discussion link' do + expect_discussion_badge_to_have_counter("0") + end + step 'I should see a discussion has started on commit diff' do page.within(".notes .discussion") do page.should have_content "#{current_user.name} started a discussion on commit" @@ -444,4 +469,10 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps def have_visible_content (text) have_css("*", text: text, visible: true) end + + def expect_discussion_badge_to_have_counter(value) + page.within(".notes-tab .badge") do + page.should have_content value + end + end end -- cgit v1.2.1 From 34564296d00408ec585bb78370cf8710a22ffdf4 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 19 Jan 2016 14:25:27 -0500 Subject: Adds `t` as a shortcut for find file anywhere --- app/assets/javascripts/dispatcher.js.coffee | 1 - app/assets/javascripts/shortcuts.js.coffee | 1 + app/assets/javascripts/shortcuts_tree.coffee | 4 ---- app/views/layouts/header/_default.html.haml | 3 +++ 4 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 app/assets/javascripts/shortcuts_tree.coffee diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 58d6b9d4060..0d88e8d254a 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -87,7 +87,6 @@ class Dispatcher new GroupAvatar() when 'projects:tree:show' new TreeView() - shortcut_handler = new ShortcutsTree() when 'projects:find_file:show' shortcut_handler = true when 'projects:blob:show' diff --git a/app/assets/javascripts/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee index 4d915bfc8c5..f141fb69c3e 100644 --- a/app/assets/javascripts/shortcuts.js.coffee +++ b/app/assets/javascripts/shortcuts.js.coffee @@ -4,6 +4,7 @@ class @Shortcuts Mousetrap.reset() Mousetrap.bind('?', @selectiveHelp) Mousetrap.bind('s', Shortcuts.focusSearch) + Mousetrap.bind('t', -> Turbolinks.visit(findFileURL)) if findFileURL? selectiveHelp: (e) => Shortcuts.showHelp(e, @enabledHelp) diff --git a/app/assets/javascripts/shortcuts_tree.coffee b/app/assets/javascripts/shortcuts_tree.coffee deleted file mode 100644 index ba0839c9fc0..00000000000 --- a/app/assets/javascripts/shortcuts_tree.coffee +++ /dev/null @@ -1,4 +0,0 @@ -class @ShortcutsTree extends ShortcutsNavigation - constructor: -> - super() - Mousetrap.bind('t', -> ShortcutsTree.findAndFollowLink('.shortcuts-find-file')) diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 3892ef8eefa..a938a0c70b9 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -37,3 +37,6 @@ %h1.title= title = render 'shared/outdated_browser' +-if defined?(@project) + :javascript + var findFileURL = '#{namespace_project_find_file_path(@project.namespace, @project, @ref)}'; \ No newline at end of file -- cgit v1.2.1 From ecbb8d73eb5fc9e3b388a2803c2bd7129c111b98 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 19 Jan 2016 14:37:00 -0500 Subject: Add reference to the correct branch. sometimes `@ref` will be `nil`. this makes sure that the correct branch is passed in. --- app/views/layouts/header/_default.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index a938a0c70b9..ec1bdf69095 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -39,4 +39,4 @@ = render 'shared/outdated_browser' -if defined?(@project) :javascript - var findFileURL = '#{namespace_project_find_file_path(@project.namespace, @project, @ref)}'; \ No newline at end of file + var findFileURL = '#{namespace_project_find_file_path(@project.namespace, @project, @ref || @project.repository.root_ref)}'; \ No newline at end of file -- cgit v1.2.1 From 60dbc4f68774049f809017a56914f465bfe026fe Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 19 Jan 2016 14:38:45 -0500 Subject: Removes `defined?` in favor to just `if` --- app/views/layouts/header/_default.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index ec1bdf69095..b62a19d43ba 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -37,6 +37,6 @@ %h1.title= title = render 'shared/outdated_browser' --if defined?(@project) +-if @project :javascript var findFileURL = '#{namespace_project_find_file_path(@project.namespace, @project, @ref || @project.repository.root_ref)}'; \ No newline at end of file -- cgit v1.2.1 From 35a776754c45b75f48c9cd08e291de35b79d1e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 19 Jan 2016 15:02:26 -0500 Subject: Update Issue/MR everytime a Note is saved/destroyed. --- app/models/note.rb | 2 +- app/services/notes/create_service.rb | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/app/models/note.rb b/app/models/note.rb index 3e1375e5ad6..15f48110ad2 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -33,7 +33,7 @@ class Note < ActiveRecord::Base participant :author belongs_to :project - belongs_to :noteable, polymorphic: true + belongs_to :noteable, polymorphic: true, touch: true belongs_to :author, class_name: "User" belongs_to :updated_by, class_name: "User" diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index b39ebac1092..a8486e6a5a1 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -10,10 +10,7 @@ module Notes # Skip system notes, like status changes and cross-references and awards unless note.system || note.is_award - event = event_service.leave_note(note, note.author) - noteable = note.noteable - - noteable.touch if event.commented? && noteable.respond_to?(:touch) + event_service.leave_note(note, note.author) note.create_cross_references! execute_hooks(note) end -- cgit v1.2.1 From 4784695860dea75864c801c6223fb348c5ac7cd3 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 19 Jan 2016 15:04:12 -0500 Subject: Proper spacing in HAML file `if` statement. --- app/views/layouts/header/_default.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index b62a19d43ba..10f80fae3c8 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -37,6 +37,6 @@ %h1.title= title = render 'shared/outdated_browser' --if @project +- if @project :javascript var findFileURL = '#{namespace_project_find_file_path(@project.namespace, @project, @ref || @project.repository.root_ref)}'; \ No newline at end of file -- cgit v1.2.1 From e5cbd5885a5cf72f406a6a4cb471b59a87865d20 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 19 Jan 2016 15:43:04 -0500 Subject: Fixes issue where other emoji (not thumbup/thumbdown) were not removed --- app/assets/javascripts/awards_handler.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 7136ef3258b..1ef31c7700e 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -44,7 +44,7 @@ class @AwardsHandler decrementCounter: (emoji) -> counter = @findEmojiIcon(emoji).siblings(".counter") emojiIcon = counter.parent() - if parseInt(counter.text()) > 0 + if parseInt(counter.text()) > 1 counter.text(parseInt(counter.text()) - 1) emojiIcon.removeClass("active") @removeMeFromAuthorList(emoji) -- cgit v1.2.1 From d4c05766c7a058cbf62186723216d754eebad92e Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 19 Jan 2016 22:18:51 +0100 Subject: Refactor GitLab Metrics docs [ci skip] --- doc/README.md | 8 +- doc/integration/README.md | 1 + doc/integration/metrics/gitlab_configuration.md | 39 +++++ .../img/metrics_gitlab_configuration_settings.png | Bin 0 -> 45148 bytes doc/integration/metrics/influxdb_configuration.md | 192 +++++++++++++++++++++ doc/integration/metrics/influxdb_schema.md | 87 ++++++++++ doc/integration/metrics/introduction.md | 64 +++++++ doc/metrics/gitlab_configuration.md | 12 -- doc/metrics/influxdb_configuration.md | 96 ----------- doc/metrics/influxdb_schema.md | 75 -------- doc/metrics/introduction.md | 55 ------ 11 files changed, 384 insertions(+), 245 deletions(-) create mode 100644 doc/integration/metrics/gitlab_configuration.md create mode 100644 doc/integration/metrics/img/metrics_gitlab_configuration_settings.png create mode 100644 doc/integration/metrics/influxdb_configuration.md create mode 100644 doc/integration/metrics/influxdb_schema.md create mode 100644 doc/integration/metrics/introduction.md delete mode 100644 doc/metrics/gitlab_configuration.md delete mode 100644 doc/metrics/influxdb_configuration.md delete mode 100644 doc/metrics/influxdb_schema.md delete mode 100644 doc/metrics/introduction.md diff --git a/doc/README.md b/doc/README.md index b9ee7de5349..bee9ab05906 100644 --- a/doc/README.md +++ b/doc/README.md @@ -49,13 +49,6 @@ - [Test Clojure applications](ci/examples/test-clojure-application.md) - Help your favorite programming language and GitLab by sending a merge request with a guide for that language. -## GitLab Metrics - -- [Introduction](metrics/introduction.md) -- [GitLab Configuration](metrics/gitlab_configuration.md) -- [InfluxDB Configuration](metrics/influxdb_configuration.md) -- [InfluxDB Schema](metrics/influxdb_schema.md) - ## Administrator documentation - [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when web hooks aren't enough. @@ -74,6 +67,7 @@ - [Reply by email](incoming_email/README.md) Allow users to comment on issues and merge requests by replying to notification emails. - [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE. - [Git LFS configuration](workflow/lfs/lfs_administration.md) +- [GitLab Metrics](integration/metrics/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics ## Contributor documentation diff --git a/doc/integration/README.md b/doc/integration/README.md index 5edac746c7b..5ba0d0fb245 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -15,6 +15,7 @@ See the documentation below for details on how to configure these services. - [OAuth2 provider](oauth_provider.md) OAuth2 application creation - [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages - [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users +- [GitLab Metrics](metrics/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics GitLab Enterprise Edition contains [advanced Jenkins support][jenkins]. diff --git a/doc/integration/metrics/gitlab_configuration.md b/doc/integration/metrics/gitlab_configuration.md new file mode 100644 index 00000000000..019db1ad02c --- /dev/null +++ b/doc/integration/metrics/gitlab_configuration.md @@ -0,0 +1,39 @@ +# GitLab Configuration + +GitLab Metrics is disabled by default. To enable it and change any of its +settings, navigate to the Admin area in **Settings > Metrics** +(`/admin/application_settings`). + +The minimum required settings you need to set are the InfluxDB host and port. +Make sure _Enable InfluxDB Metrics_ is checked and hit **Save** to save the +changes. + +--- + +![GitLab Metrics Admin Settings](img/metrics_gitlab_configuration_settings.png) + +--- + +Finally, a restart of all GitLab processes is required for the changes to take +effect: + +```bash +# For Omnibus installations +sudo gitlab-ctl restart + +# For installations from source +sudo service gitlab restart +``` + +## Pending Migrations + +When any migrations are pending, the metrics are disabled until the migrations +have been performed. + +--- + +Read more on: + +- [Introduction to GitLab Metrics](introduction.md) +- [InfluxDB Configuration](influxdb_configuration.md) +- [InfluxDB Schema](influxdb_schema.md) diff --git a/doc/integration/metrics/img/metrics_gitlab_configuration_settings.png b/doc/integration/metrics/img/metrics_gitlab_configuration_settings.png new file mode 100644 index 00000000000..14d82b6ac98 Binary files /dev/null and b/doc/integration/metrics/img/metrics_gitlab_configuration_settings.png differ diff --git a/doc/integration/metrics/influxdb_configuration.md b/doc/integration/metrics/influxdb_configuration.md new file mode 100644 index 00000000000..13227f1e889 --- /dev/null +++ b/doc/integration/metrics/influxdb_configuration.md @@ -0,0 +1,192 @@ +# InfluxDB Configuration + +The default settings provided by [InfluxDB] are not sufficient for a high traffic +GitLab environment. The settings discussed in this document are based on the +settings GitLab uses for GitLab.com, depending on your own needs you may need to +further adjust them. + +If you are intending to run InfluxDB on the same server as GitLab, make sure +you have plenty of RAM since InfluxDB can use quite a bit depending on traffic. + +Unless you are going with a budget setup, it's advised to run it separately. + +## Requirements + +- InfluxDB 0.9.5 or newer +- A fairly modern version of Linux +- At least 4GB of RAM +- At least 10GB of storage for InfluxDB data + +Note that the RAM and storage requirements can differ greatly depending on the +amount of data received/stored. To limit the amount of stored data users can +look into [InfluxDB Retention Policies][influxdb-retention]. + +## Installation + +Installing InfluxDB is out of the scope of this document. Please refer to the +[InfluxDB documentation]. + +## InfluxDB Server Settings + +Since InfluxDB has many settings that users may wish to customize themselves +(e.g. what port to run InfluxDB on), we'll only cover the essentials. + +The configuration file in question is usually located at +`/etc/influxdb/influxdb.conf`. Whenever you make a change in this file, +InfluxDB needs to be restarted. + +### Storage Engine + +InfluxDB comes with different storage engines and as of InfluxDB 0.9.5 a new +storage engine is available, called [TSM Tree]. All users **must** use the new +`tsm1` storage engine as this [will be the default engine][tsm1-commit] in +upcoming InfluxDB releases. + +Make sure you have the following in your configuration file: + +``` +[data] + dir = "/var/lib/influxdb/data" + engine = "tsm1" +``` + +### Admin Panel + +Production environments should have the InfluxDB admin panel **disabled**. This +feature can be disabled by adding the following to your InfluxDB configuration +file: + +``` +[admin] + enabled = false +``` + +### HTTP + +HTTP is required when using the [InfluxDB CLI] or other tools such as Grafana, +thus it should be enabled. When enabling make sure to _also_ enable +authentication: + +``` +[http] + enabled = true + auth-enabled = true +``` + +_**Note:** Before you enable authentication, you might want to [create an +admin user](#create-a-new-admin-user)._ + +### UDP + +GitLab writes data to InfluxDB via UDP and thus this must be enabled. Enabling +UDP can be done using the following settings: + +``` +[[udp]] + enabled = true + bind-address = ":8089" + database = "gitlab" + batch-size = 1000 + batch-pending = 5 + batch-timeout = "1s" + read-buffer = 209715200 +``` + +This does the following: + +1. Enable UDP and bind it to port 8089 for all addresses. +2. Store any data received in the "gitlab" database. +3. Define a batch of points to be 1000 points in size and allow a maximum of + 5 batches _or_ flush them automatically after 1 second. +4. Define a UDP read buffer size of 200 MB. + +One of the most important settings here is the UDP read buffer size as if this +value is set too low, packets will be dropped. You must also make sure the OS +buffer size is set to the same value, the default value is almost never enough. + +To set the OS buffer size to 200 MB, on Linux you can run the following command: + +```bash +sysctl -w net.core.rmem_max=209715200 +``` + +To make this permanent, add the following to `/etc/sysctl.conf` and restart the +server: + +```bash +net.core.rmem_max=209715200 +``` + +It is **very important** to make sure the buffer sizes are large enough to +handle all data sent to InfluxDB as otherwise you _will_ lose data. The above +buffer sizes are based on the traffic for GitLab.com. Depending on the amount of +traffic, users may be able to use a smaller buffer size, but we highly recommend +using _at least_ 100 MB. + +When enabling UDP, users should take care to not expose the port to the public, +as doing so will allow anybody to write data into your InfluxDB database (as +[InfluxDB's UDP protocol][udp] doesn't support authentication). We recommend either +whitelisting the allowed IP addresses/ranges, or setting up a VLAN and only +allowing traffic from members of said VLAN. + +## Create a new admin user + +If you want to [enable authentication](#http), you might want to [create an +admin user][influx-admin]: + +``` +influx -execute "CREATE USER thedude WITH PASSWORD '1234' WITH ALL PRIVILEGES" +``` + +## Create the `gitlab` database + +Once you get InfluxDB up and running, you need to create a database for GitLab. +Make sure you have changed the [storage engine](#storage-engine) to `tsm1` +before creating a database. + +_**Note:** If you [created an admin user](#create-a-new-admin-user) and enabled +[HTTP authentication](#http), remember to append the username (`-username thedude`) +and password (`-password 1234`) to the commands below._ + +Run the following command to create a database named `gitlab`: + +```bash +influx -execute 'CREATE DATABASE gitlab' +``` + +The name **must** be `gitlab`, do not use any other name. + +Next, make sure that the database was successfully created: + +```bash +influx -execute 'SHOW DATABASES' +``` + +The output should be similar to: + +``` +name: databases +--------------- +name +_internal +gitlab +``` + +That's it! Now your GitLab instance should send data to InfluxDB. + +--- + +Read more on: + +- [Introduction to GitLab Metrics](introduction.md) +- [GitLab Configuration](gitlab_configuration.md) +- [InfluxDB Schema](influxdb_schema.md) + +[influxdb-retention]: https://docs.influxdata.com/influxdb/v0.9/query_language/database_management/#retention-policy-management +[influxdb documentation]: https://docs.influxdata.com/influxdb/v0.9/ +[influxdb cli]: https://docs.influxdata.com/influxdb/v0.9/tools/shell/ +[udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/ +[influxdb]: https://influxdata.com/time-series-platform/influxdb/ +[tsm tree]: https://influxdata.com/blog/new-storage-engine-time-structured-merge-tree/ +[tsm1-commit]: https://github.com/influxdata/influxdb/commit/15d723dc77651bac83e09e2b1c94be480966cb0d +[influx-admin]: https://docs.influxdata.com/influxdb/v0.9/administration/authentication_and_authorization/#create-a-new-admin-user diff --git a/doc/integration/metrics/influxdb_schema.md b/doc/integration/metrics/influxdb_schema.md new file mode 100644 index 00000000000..a9ef0b446c8 --- /dev/null +++ b/doc/integration/metrics/influxdb_schema.md @@ -0,0 +1,87 @@ +# InfluxDB Schema + +The following measurements are currently stored in InfluxDB: + +- `PROCESS_file_descriptors` +- `PROCESS_gc_statistics` +- `PROCESS_memory_usage` +- `PROCESS_method_calls` +- `PROCESS_object_counts` +- `PROCESS_transactions` +- `PROCESS_views` + +Here, `PROCESS` is replaced with either `rails` or `sidekiq` depending on the +process type. In all series, any form of duration is stored in milliseconds. + +## PROCESS_file_descriptors + +This measurement contains the number of open file descriptors over time. The +value field `value` contains the number of descriptors. + +## PROCESS_gc_statistics + +This measurement contains Ruby garbage collection statistics such as the amount +of minor/major GC runs (relative to the last sampling interval), the time spent +in garbage collection cycles, and all fields/values returned by `GC.stat`. + +## PROCESS_memory_usage + +This measurement contains the process' memory usage (in bytes) over time. The +value field `value` contains the number of bytes. + +## PROCESS_method_calls + +This measurement contains the methods called during a transaction along with +their duration, and a name of the transaction action that invoked the method (if +available). The method call duration is stored in the value field `duration`, +while the method name is stored in the tag `method`. The tag `action` contains +the full name of the transaction action. Both the `method` and `action` fields +are in the following format: + +``` +ClassName#method_name +``` + +For example, a method called by the `show` method in the `UsersController` class +would have `action` set to `UsersController#show`. + +## PROCESS_object_counts + +This measurement is used to store retained Ruby objects (per class) and the +amount of retained objects. The number of objects is stored in the `count` value +field while the class name is stored in the `type` tag. + +## PROCESS_transactions + +This measurement is used to store basic transaction details such as the time it +took to complete a transaction, how much time was spent in SQL queries, etc. The +following value fields are available: + +| Value | Description | +| ----- | ----------- | +| `duration` | The total duration of the transaction | +| `allocated_memory` | The amount of bytes allocated while the transaction was running. This value is only reliable when using single-threaded application servers | +| `method_duration` | The total time spent in method calls | +| `sql_duration` | The total time spent in SQL queries | +| `view_duration` | The total time spent in views | + +## PROCESS_views + +This measurement is used to store view rendering timings for a transaction. The +following value fields are available: + +| Value | Description | +| ----- | ----------- | +| `duration` | The rendering time of the view | +| `view` | The path of the view, relative to the application's root directory | + +The `action` tag contains the action name of the transaction that rendered the +view. + +--- + +Read more on: + +- [Introduction to GitLab Metrics](introduction.md) +- [GitLab Configuration](gitlab_configuration.md) +- [InfluxDB Configuration](influxdb_configuration.md) diff --git a/doc/integration/metrics/introduction.md b/doc/integration/metrics/introduction.md new file mode 100644 index 00000000000..bf388aa2bba --- /dev/null +++ b/doc/integration/metrics/introduction.md @@ -0,0 +1,64 @@ +# GitLab Metrics + +GitLab comes with its own application performance measuring system as of GitLab +8.4, simply called "GitLab Metrics". GitLab Metrics is available in both the +Community and Enterprise editions. + +Apart from this introduction, you are advised to read through the following +documents in order to understand and properly configure GitLab Metrics: + +- [GitLab Configuration](gitlab_configuration.md) +- [InfluxDB Configuration](influxdb_configuration.md) +- [InfluxDB Schema](influxdb_schema.md) + +## Introduction to GitLab Metrics + +GitLab Metrics makes it possible to measure a wide variety of statistics +including (but not limited to): + +- The time it took to complete a transaction (a web request or Sidekiq job). +- The time spent in running SQL queries and rendering HAML views. +- The time spent executing (instrumented) Ruby methods. +- Ruby object allocations, and retained objects in particular. +- System statistics such as the process' memory usage and open file descriptors. +- Ruby garbage collection statistics. + +Metrics data is written to [InfluxDB][influxdb] over [UDP][influxdb-udp]. Stored +data can be visualized using [Grafana][grafana] or any other application that +supports reading data from InfluxDB. Alternatively data can be queried using the +InfluxDB CLI. + +## Metric Types + +Two types of metrics are collected: + +1. Transaction specific metrics. +1. Sampled metrics, collected at a certain interval in a separate thread. + +### Transaction Metrics + +Transaction metrics are metrics that can be associated with a single +transaction. This includes statistics such as the transaction duration, timings +of any executed SQL queries, time spent rendering HAML views, etc. These metrics +are collected for every Rack request and Sidekiq job processed. + +### Sampled Metrics + +Sampled metrics are metrics that can't be associated with a single transaction. +Examples include garbage collection statistics and retained Ruby objects. These +metrics are collected at a regular interval. This interval is made up out of two +parts: + +1. A user defined interval. +1. A randomly generated offset added on top of the interval, the same offset + can't be used twice in a row. + +The actual interval can be anywhere between a half of the defined interval and a +half above the interval. For example, for a user defined interval of 15 seconds +the actual interval can be anywhere between 7.5 and 22.5. The interval is +re-generated for every sampling run instead of being generated once and re-used +for the duration of the process' lifetime. + +[influxdb]: https://influxdata.com/time-series-platform/influxdb/ +[influxdb-udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/ +[grafana]: http://grafana.org/ diff --git a/doc/metrics/gitlab_configuration.md b/doc/metrics/gitlab_configuration.md deleted file mode 100644 index 3b44b4394ff..00000000000 --- a/doc/metrics/gitlab_configuration.md +++ /dev/null @@ -1,12 +0,0 @@ -# GitLab Configuration - -By default GitLab Metrics is disabled. To enable GitLab Metrics and change any -of its settings open a web browser and navigate to -`http://YOUR_GITLAB_HOST/admin/application_settings`, the settings can be found -in the "Metrics" section. A restart of all GitLab processes is required for any -changes to take effect. - -## Pending Migrations - -When any migrations are pending the metrics are disabled until the migrations -have been performed. diff --git a/doc/metrics/influxdb_configuration.md b/doc/metrics/influxdb_configuration.md deleted file mode 100644 index c67c0f15108..00000000000 --- a/doc/metrics/influxdb_configuration.md +++ /dev/null @@ -1,96 +0,0 @@ -# InfluxDB Configuration - -The default settings provided by InfluxDB are not sufficient for a high traffic -GitLab environment. The settings discussed in this document are based on the -settings GitLab uses for GitLab.com, depending on your own needs you may need to -further adjust them. - -## Requirements - -* InfluxDB 0.9 or newer -* A fairly modern version of Linux -* At least 4GB of RAM -* At least 10GB of storage for InfluxDB data - -Note that the RAM and storage requirements can differ greatly depending on the -amount of data received/stored. To limit the amount of stored data users can -look into [InfluxDB Retention Policies][influxdb-retention]. - -## InfluxDB Server Settings - -Since InfluxDB has many settings that users may wish to customize themselves -(e.g. what port to run InfluxDB on) we'll only cover the essentials. - -### Storage Engine - -InfluxDB comes with different storage engines and as of InfluxDB 0.9 a new -storage engine is available called "tsm1". All users _must_ use the new tsm1 -storage engine (this will be the default engine in upcoming InfluxDB engines). - -### Admin Panel - -Production environments should have the InfluxDB admin panel _disabled_. This -feature can be disabled by adding the following to your InfluxDB configuration -file: - - [admin] - enabled = false - -### HTTP - -HTTP is required when using the InfluxDB CLI or other tools such as Grafana, -thus it should be enabled. When enabling make sure to _also_ enable -authentication: - - [http] - enabled = true - auth-enabled = true - -### UDP - -GitLab writes data to InfluxDB via UDP and thus this must be enabled. Enabling -UDP can be done using the following settings: - - [udp] - enabled = true - bind-address = ":8089" - database = "gitlab" - batch-size = 1000 - batch-pending = 5 - batch-timeout = 1s - read-buffer = 209715200 - -This does the following: - -1. Enable UDP and bind it to port 8089 for all addresses. -2. Store any data received in the "gitlab" database. -3. Define a batch of points to be 1000 points in size and allow a maximum of - 5 batches _or_ flush them automatically after 1 second. -4. Define a UDP read buffer size of 200 MB. - -One of the most important settings here is the UDP read buffer size as if this -value is set too low packets will be dropped. You must also make sure the OS -buffer size is set to the same value, the default value is almost never enough. - -To set the OS buffer size to 200 MB on Linux you can run the following command: - - sysctl -w net.core.rmem_max=209715200 - -To make this permanent, add the following to `/etc/sysctl.conf` and restart the -server: - - net.core.rmem_max=209715200 - -It is **very important** to make sure the buffer sizes are large enough to -handle all data sent to InfluxDB as otherwise you _will_ lose data. The above -buffer sizes are based on the traffic for GitLab.com. Depending on the amount of -traffic users may be able to use a smaller buffer size, but we highly recommend -using _at least_ 100 MB. - -When enabling UDP users should take care to not expose the port to the public as -doing so will allow anybody to write data into your InfluxDB database (as -InfluxDB's UDP protocol doesn't support authentication). We recommend either -whitelisting the allowed IP addresses/ranges, or setting up a VLAN and only -allowing traffic from members of said VLAN. - -[influxdb-retention]: https://docs.influxdata.com/influxdb/v0.9/query_language/database_management/#retention-policy-management diff --git a/doc/metrics/influxdb_schema.md b/doc/metrics/influxdb_schema.md deleted file mode 100644 index a8e69b5e173..00000000000 --- a/doc/metrics/influxdb_schema.md +++ /dev/null @@ -1,75 +0,0 @@ -# InfluxDB Schema - -The following measurements are currently stored in InfluxDB: - -* `PROCESS_file_descriptors` -* `PROCESS_gc_statistics` -* `PROCESS_memory_usage` -* `PROCESS_method_calls` -* `PROCESS_object_counts` -* `PROCESS_transactions` -* `PROCESS_views` - -Here `PROCESS` is replaced with either "rails" or "sidekiq" depending on the -process type. In all series any form of duration is stored in milliseconds. - -## PROCESS_file_descriptors - -This measurement contains the number of open file descriptors over time. The -value field `value` contains the number of descriptors. - -## PROCESS_gc_statistics - -This measurement contains Ruby garbage collection statistics such as the amount -of minor/major GC runs (relative to the last sampling interval), the time spent -in garbage collection cycles, and all fields/values returned by `GC.stat`. - -## PROCESS_memory_usage - -This measurement contains the process' memory usage (in bytes) over time. The -value field `value` contains the number of bytes. - -## PROCESS_method_calls - -This measurement contains the methods called during a transaction along with -their durations and a name of the transaction action that invoked the method (if -available). The method call duration is stored in the value field `duration` -while the method name is stored in the tag `method`. The tag `action` contains -the full name of the transaction action. Both the `method` and `action` fields -are in the following format: - - ClassName#method_name - -For example, a method called by the `show` method in the `UsersController` class -would have `action` set to `UsersController#show`. - -## PROCESS_object_counts - -This measurement is used to store retained Ruby objects (per class) and the -amount of retained objects. The number of objects is stored in the `count` value -field while the class name is stored in the `type` tag. - -## PROCESS_transactions - -This measurement is used to store basic transaction details such as the time it -took to complete a transaction, how much time was spent in SQL queries, etc. The -following value fields are available: - -* `duration`: the total duration of the transaction. -* `allocated_memory`: the amount of bytes allocated while the transaction was - running. This value is only reliable when using single-threaded application - servers. -* `method_duration`: the total time spent in method calls. -* `sql_duration`: the total time spent in SQL queries. -* `view_duration`: the total time spent in views. - -## PROCESS_views - -This measurement is used to store view rendering timings for a transaction. The -following value fields are available: - -* `duration`: the rendering time of the view. -* `view`: the path of the view, relative to the application's root directory. - -The `action` tag contains the action name of the transaction that rendered the -view. diff --git a/doc/metrics/introduction.md b/doc/metrics/introduction.md deleted file mode 100644 index 007fff9e7b4..00000000000 --- a/doc/metrics/introduction.md +++ /dev/null @@ -1,55 +0,0 @@ -# Introduction to GitLab Metrics - -GitLab comes with its own application performance measuring system as of GitLab -8.4, simply called "GitLab Metrics". GitLab Metrics is available in both the -Community and Enterprise editions. - -GitLab Metrics makes it possible to measure a wide variety of statistics -including (but not limited to): - -* The time it took to complete a transaction (a web request or Sidekiq job). -* The time spent in running SQL queries and rendering HAML views. -* The time spent executing (instrumented) Ruby methods. -* Ruby object allocations, and retained objects in particular. -* System statistics such as the process' memory usage and open file descriptors. -* Ruby garbage collection statistics. - -Metrics data is written to [InfluxDB][influxdb] over [UDP](influxdb-udp). Stored -data can be visualized using [Grafana][grafana] or any other application that -supports reading data from InfluxDB. Alternatively data can be queried using the -InfluxDB CLI. - -## Metric Types - -Two types of metrics are collected: - -1. Transaction specific metrics. -2. Sampled metrics, collected at a certain interval in a separate thread. - -### Transaction Metrics - -Transaction metrics are metrics that can be associated with a single -transaction. This includes statistics such as the transaction duration, timings -of any executed SQL queries, time spent rendering HAML views, etc. These metrics -are collected for every Rack request and Sidekiq job processed. - -### Sampled Metrics - -Sampled metrics are metrics that can't be associated with a single transaction. -Examples include garbage collection statistics and retained Ruby objects. These -metrics are collected at a regular interval. This interval is made up out of two -parts: - -1. A user defined interval. -2. A randomly generated offset added on top of the interval, the same offset - can't be used twice in a row. - -The actual interval can be anywhere between a half of the defined interval and a -half above the interval. For example, for a user defined interval of 15 seconds -the actual interval can be anywhere between 7.5 and 22.5. The interval is -re-generated for every sampling run instead of being generated once and re-used -for the duration of the process' lifetime. - -[influxdb]: https://influxdata.com/time-series-platform/influxdb/ -[influxdb-udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/ -[grafana]: http://grafana.org/ -- cgit v1.2.1 From c26816f7a732b339b14d7d6d928f5a86f490d77c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 19 Jan 2016 17:35:43 -0500 Subject: Fix broken specs. --- spec/lib/gitlab/note_data_builder_spec.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/spec/lib/gitlab/note_data_builder_spec.rb b/spec/lib/gitlab/note_data_builder_spec.rb index 6cbdae737f4..691f36e6cb7 100644 --- a/spec/lib/gitlab/note_data_builder_spec.rb +++ b/spec/lib/gitlab/note_data_builder_spec.rb @@ -37,7 +37,8 @@ describe 'Gitlab::NoteDataBuilder', lib: true do it 'returns the note and issue-specific data' do expect(data).to have_key(:issue) - expect(data[:issue]).to eq(issue.hook_attrs) + expect(data[:issue].except('updated_at')).to eq(issue.hook_attrs.except('updated_at')) + expect(data[:issue]['updated_at']).to be > issue.hook_attrs['updated_at'] end end @@ -47,7 +48,8 @@ describe 'Gitlab::NoteDataBuilder', lib: true do it 'returns the note and merge request data' do expect(data).to have_key(:merge_request) - expect(data[:merge_request]).to eq(merge_request.hook_attrs) + expect(data[:merge_request].except('updated_at')).to eq(merge_request.hook_attrs.except('updated_at')) + expect(data[:merge_request]['updated_at']).to be > merge_request.hook_attrs['updated_at'] end end @@ -57,7 +59,8 @@ describe 'Gitlab::NoteDataBuilder', lib: true do it 'returns the note and merge request diff data' do expect(data).to have_key(:merge_request) - expect(data[:merge_request]).to eq(merge_request.hook_attrs) + expect(data[:merge_request].except('updated_at')).to eq(merge_request.hook_attrs.except('updated_at')) + expect(data[:merge_request]['updated_at']).to be > merge_request.hook_attrs['updated_at'] end end @@ -67,7 +70,8 @@ describe 'Gitlab::NoteDataBuilder', lib: true do it 'returns the note and project snippet data' do expect(data).to have_key(:snippet) - expect(data[:snippet]).to eq(snippet.hook_attrs) + expect(data[:snippet].except('updated_at')).to eq(snippet.hook_attrs.except('updated_at')) + expect(data[:snippet]['updated_at']).to be > snippet.hook_attrs['updated_at'] end end end -- cgit v1.2.1 From 70e59035c0803bbed29d69fe1c535a15f0249f53 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 19 Jan 2016 21:39:48 -0500 Subject: Fix project find url so branch ref is not nil Also adds autocomplete="off" to find input. --- app/views/layouts/header/_default.html.haml | 8 ++++++-- app/views/projects/find_file/show.html.haml | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 10f80fae3c8..b83b5b1c9b0 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -37,6 +37,10 @@ %h1.title= title = render 'shared/outdated_browser' -- if @project +-if @project && !@project.empty_repo? && @ref :javascript - var findFileURL = '#{namespace_project_find_file_path(@project.namespace, @project, @ref || @project.repository.root_ref)}'; \ No newline at end of file + var findFileURL = '#{namespace_project_find_file_path(@project.namespace, @project, @ref)}'; + +-elsif @project && !@project.empty_repo? + :javascript + var findFileURL = '#{namespace_project_find_file_path(@project.namespace, @project, @project.repository.root_ref)}'; \ No newline at end of file diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml index 40a2a61d8a1..905f6bbbd48 100644 --- a/app/views/projects/find_file/show.html.haml +++ b/app/views/projects/find_file/show.html.haml @@ -10,7 +10,7 @@ = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do = @project.path %li.file-finder - %input#file_find.form-control.file-finder-input{type: "text", placeholder: 'Find by path'} + %input#file_find.form-control.file-finder-input{type: "text", placeholder: 'Find by path', autocomplete: 'off'} %div.tree-content-holder .table-holder -- cgit v1.2.1 From 2922dfce79476fe1c4330d1a27eb908010cb5964 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 17 Jan 2016 15:39:51 -0500 Subject: Don't vendor minified Chart.js The filename is titlecased because that's how it came from the vendor, and we're not touching it. --- app/assets/javascripts/application.js.coffee | 2 +- vendor/assets/javascripts/Chart.js | 3477 ++++++++++++++++++++++++++ vendor/assets/javascripts/chart-lib.min.js | 11 - 3 files changed, 3478 insertions(+), 12 deletions(-) create mode 100755 vendor/assets/javascripts/Chart.js delete mode 100644 vendor/assets/javascripts/chart-lib.min.js diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index c095e5ae2b1..5e5c5a916a8 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -23,7 +23,7 @@ #= require raphael #= require g.raphael-min #= require g.bar-min -#= require chart-lib.min +#= require Chart #= require branch-graph #= require ace/ace #= require ace/ext-searchbox diff --git a/vendor/assets/javascripts/Chart.js b/vendor/assets/javascripts/Chart.js new file mode 100755 index 00000000000..c264262ba73 --- /dev/null +++ b/vendor/assets/javascripts/Chart.js @@ -0,0 +1,3477 @@ +/*! + * Chart.js + * http://chartjs.org/ + * Version: 1.0.2 + * + * Copyright 2015 Nick Downie + * Released under the MIT license + * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md + */ + + +(function(){ + + "use strict"; + + //Declare root variable - window in the browser, global on the server + var root = this, + previous = root.Chart; + + //Occupy the global variable of Chart, and create a simple base class + var Chart = function(context){ + var chart = this; + this.canvas = context.canvas; + + this.ctx = context; + + //Variables global to the chart + var computeDimension = function(element,dimension) + { + if (element['offset'+dimension]) + { + return element['offset'+dimension]; + } + else + { + return document.defaultView.getComputedStyle(element).getPropertyValue(dimension); + } + } + + var width = this.width = computeDimension(context.canvas,'Width'); + var height = this.height = computeDimension(context.canvas,'Height'); + + // Firefox requires this to work correctly + context.canvas.width = width; + context.canvas.height = height; + + var width = this.width = context.canvas.width; + var height = this.height = context.canvas.height; + this.aspectRatio = this.width / this.height; + //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. + helpers.retinaScale(this); + + return this; + }; + //Globally expose the defaults to allow for user updating/changing + Chart.defaults = { + global: { + // Boolean - Whether to animate the chart + animation: true, + + // Number - Number of animation steps + animationSteps: 60, + + // String - Animation easing effect + animationEasing: "easeOutQuart", + + // Boolean - If we should show the scale at all + showScale: true, + + // Boolean - If we want to override with a hard coded scale + scaleOverride: false, + + // ** Required if scaleOverride is true ** + // Number - The number of steps in a hard coded scale + scaleSteps: null, + // Number - The value jump in the hard coded scale + scaleStepWidth: null, + // Number - The scale starting value + scaleStartValue: null, + + // String - Colour of the scale line + scaleLineColor: "rgba(0,0,0,.1)", + + // Number - Pixel width of the scale line + scaleLineWidth: 1, + + // Boolean - Whether to show labels on the scale + scaleShowLabels: true, + + // Interpolated JS string - can access value + scaleLabel: "<%=value%>", + + // Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there + scaleIntegersOnly: true, + + // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value + scaleBeginAtZero: false, + + // String - Scale label font declaration for the scale label + scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Scale label font size in pixels + scaleFontSize: 12, + + // String - Scale label font weight style + scaleFontStyle: "normal", + + // String - Scale label font colour + scaleFontColor: "#666", + + // Boolean - whether or not the chart should be responsive and resize when the browser does. + responsive: false, + + // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container + maintainAspectRatio: true, + + // Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove + showTooltips: true, + + // Boolean - Determines whether to draw built-in tooltip or call custom tooltip function + customTooltips: false, + + // Array - Array of string names to attach tooltip events + tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"], + + // String - Tooltip background colour + tooltipFillColor: "rgba(0,0,0,0.8)", + + // String - Tooltip label font declaration for the scale label + tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Tooltip label font size in pixels + tooltipFontSize: 14, + + // String - Tooltip font weight style + tooltipFontStyle: "normal", + + // String - Tooltip label font colour + tooltipFontColor: "#fff", + + // String - Tooltip title font declaration for the scale label + tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Tooltip title font size in pixels + tooltipTitleFontSize: 14, + + // String - Tooltip title font weight style + tooltipTitleFontStyle: "bold", + + // String - Tooltip title font colour + tooltipTitleFontColor: "#fff", + + // Number - pixel width of padding around tooltip text + tooltipYPadding: 6, + + // Number - pixel width of padding around tooltip text + tooltipXPadding: 6, + + // Number - Size of the caret on the tooltip + tooltipCaretSize: 8, + + // Number - Pixel radius of the tooltip border + tooltipCornerRadius: 6, + + // Number - Pixel offset from point x to tooltip edge + tooltipXOffset: 10, + + // String - Template string for single tooltips + tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>", + + // String - Template string for single tooltips + multiTooltipTemplate: "<%= value %>", + + // String - Colour behind the legend colour block + multiTooltipKeyBackground: '#fff', + + // Function - Will fire on animation progression. + onAnimationProgress: function(){}, + + // Function - Will fire on animation completion. + onAnimationComplete: function(){} + + } + }; + + //Create a dictionary of chart types, to allow for extension of existing types + Chart.types = {}; + + //Global Chart helpers object for utility methods and classes + var helpers = Chart.helpers = {}; + + //-- Basic js utility methods + var each = helpers.each = function(loopable,callback,self){ + var additionalArgs = Array.prototype.slice.call(arguments, 3); + // Check to see if null or undefined firstly. + if (loopable){ + if (loopable.length === +loopable.length){ + var i; + for (i=0; i= 0; i--) { + var currentItem = arrayToSearch[i]; + if (filterCallback(currentItem)){ + return currentItem; + } + } + }, + inherits = helpers.inherits = function(extensions){ + //Basic javascript inheritance based on the model created in Backbone.js + var parent = this; + var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); }; + + var Surrogate = function(){ this.constructor = ChartElement;}; + Surrogate.prototype = parent.prototype; + ChartElement.prototype = new Surrogate(); + + ChartElement.extend = inherits; + + if (extensions) extend(ChartElement.prototype, extensions); + + ChartElement.__super__ = parent.prototype; + + return ChartElement; + }, + noop = helpers.noop = function(){}, + uid = helpers.uid = (function(){ + var id=0; + return function(){ + return "chart-" + id++; + }; + })(), + warn = helpers.warn = function(str){ + //Method for warning of errors + if (window.console && typeof window.console.warn == "function") console.warn(str); + }, + amd = helpers.amd = (typeof define == 'function' && define.amd), + //-- Math methods + isNumber = helpers.isNumber = function(n){ + return !isNaN(parseFloat(n)) && isFinite(n); + }, + max = helpers.max = function(array){ + return Math.max.apply( Math, array ); + }, + min = helpers.min = function(array){ + return Math.min.apply( Math, array ); + }, + cap = helpers.cap = function(valueToCap,maxValue,minValue){ + if(isNumber(maxValue)) { + if( valueToCap > maxValue ) { + return maxValue; + } + } + else if(isNumber(minValue)){ + if ( valueToCap < minValue ){ + return minValue; + } + } + return valueToCap; + }, + getDecimalPlaces = helpers.getDecimalPlaces = function(num){ + if (num%1!==0 && isNumber(num)){ + return num.toString().split(".")[1].length; + } + else { + return 0; + } + }, + toRadians = helpers.radians = function(degrees){ + return degrees * (Math.PI/180); + }, + // Gets the angle from vertical upright to the point about a centre. + getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){ + var distanceFromXCenter = anglePoint.x - centrePoint.x, + distanceFromYCenter = anglePoint.y - centrePoint.y, + radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); + + + var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter); + + //If the segment is in the top left quadrant, we need to add another rotation to the angle + if (distanceFromXCenter < 0 && distanceFromYCenter < 0){ + angle += Math.PI*2; + } + + return { + angle: angle, + distance: radialDistanceFromCenter + }; + }, + aliasPixel = helpers.aliasPixel = function(pixelWidth){ + return (pixelWidth % 2 === 0) ? 0 : 0.5; + }, + splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){ + //Props to Rob Spencer at scaled innovation for his post on splining between points + //http://scaledinnovation.com/analytics/splines/aboutSplines.html + var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)), + d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)), + fa=t*d01/(d01+d12),// scaling factor for triangle Ta + fb=t*d12/(d01+d12); + return { + inner : { + x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x), + y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y) + }, + outer : { + x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x), + y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y) + } + }; + }, + calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){ + return Math.floor(Math.log(val) / Math.LN10); + }, + calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){ + + //Set a minimum step of two - a point at the top of the graph, and a point at the base + var minSteps = 2, + maxSteps = Math.floor(drawingSize/(textSize * 1.5)), + skipFitting = (minSteps >= maxSteps); + + var maxValue = max(valuesArray), + minValue = min(valuesArray); + + // We need some degree of seperation here to calculate the scales if all the values are the same + // Adding/minusing 0.5 will give us a range of 1. + if (maxValue === minValue){ + maxValue += 0.5; + // So we don't end up with a graph with a negative start value if we've said always start from zero + if (minValue >= 0.5 && !startFromZero){ + minValue -= 0.5; + } + else{ + // Make up a whole number above the values + maxValue += 0.5; + } + } + + var valueRange = Math.abs(maxValue - minValue), + rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange), + graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), + graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), + graphRange = graphMax - graphMin, + stepValue = Math.pow(10, rangeOrderOfMagnitude), + numberOfSteps = Math.round(graphRange / stepValue); + + //If we have more space on the graph we'll use it to give more definition to the data + while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) { + if(numberOfSteps > maxSteps){ + stepValue *=2; + numberOfSteps = Math.round(graphRange/stepValue); + // Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps. + if (numberOfSteps % 1 !== 0){ + skipFitting = true; + } + } + //We can fit in double the amount of scale points on the scale + else{ + //If user has declared ints only, and the step value isn't a decimal + if (integersOnly && rangeOrderOfMagnitude >= 0){ + //If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float + if(stepValue/2 % 1 === 0){ + stepValue /=2; + numberOfSteps = Math.round(graphRange/stepValue); + } + //If it would make it a float break out of the loop + else{ + break; + } + } + //If the scale doesn't have to be an int, make the scale more granular anyway. + else{ + stepValue /=2; + numberOfSteps = Math.round(graphRange/stepValue); + } + + } + } + + if (skipFitting){ + numberOfSteps = minSteps; + stepValue = graphRange / numberOfSteps; + } + + return { + steps : numberOfSteps, + stepValue : stepValue, + min : graphMin, + max : graphMin + (numberOfSteps * stepValue) + }; + + }, + /* jshint ignore:start */ + // Blows up jshint errors based on the new Function constructor + //Templating methods + //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/ + template = helpers.template = function(templateString, valuesObject){ + + // If templateString is function rather than string-template - call the function for valuesObject + + if(templateString instanceof Function){ + return templateString(valuesObject); + } + + var cache = {}; + function tmpl(str, data){ + // Figure out if we're getting a template, or if we need to + // load the template - and be sure to cache the result. + var fn = !/\W/.test(str) ? + cache[str] = cache[str] : + + // Generate a reusable function that will serve as a template + // generator (and which will be cached). + new Function("obj", + "var p=[],print=function(){p.push.apply(p,arguments);};" + + + // Introduce the data as local variables using with(){} + "with(obj){p.push('" + + + // Convert the template into pure JavaScript + str + .replace(/[\r\t\n]/g, " ") + .split("<%").join("\t") + .replace(/((^|%>)[^\t]*)'/g, "$1\r") + .replace(/\t=(.*?)%>/g, "',$1,'") + .split("\t").join("');") + .split("%>").join("p.push('") + .split("\r").join("\\'") + + "');}return p.join('');" + ); + + // Provide some basic currying to the user + return data ? fn( data ) : fn; + } + return tmpl(templateString,valuesObject); + }, + /* jshint ignore:end */ + generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){ + var labelsArray = new Array(numberOfSteps); + if (labelTemplateString){ + each(labelsArray,function(val,index){ + labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))}); + }); + } + return labelsArray; + }, + //--Animation methods + //Easing functions adapted from Robert Penner's easing equations + //http://www.robertpenner.com/easing/ + easingEffects = helpers.easingEffects = { + linear: function (t) { + return t; + }, + easeInQuad: function (t) { + return t * t; + }, + easeOutQuad: function (t) { + return -1 * t * (t - 2); + }, + easeInOutQuad: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t; + return -1 / 2 * ((--t) * (t - 2) - 1); + }, + easeInCubic: function (t) { + return t * t * t; + }, + easeOutCubic: function (t) { + return 1 * ((t = t / 1 - 1) * t * t + 1); + }, + easeInOutCubic: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t; + return 1 / 2 * ((t -= 2) * t * t + 2); + }, + easeInQuart: function (t) { + return t * t * t * t; + }, + easeOutQuart: function (t) { + return -1 * ((t = t / 1 - 1) * t * t * t - 1); + }, + easeInOutQuart: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t; + return -1 / 2 * ((t -= 2) * t * t * t - 2); + }, + easeInQuint: function (t) { + return 1 * (t /= 1) * t * t * t * t; + }, + easeOutQuint: function (t) { + return 1 * ((t = t / 1 - 1) * t * t * t * t + 1); + }, + easeInOutQuint: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t * t; + return 1 / 2 * ((t -= 2) * t * t * t * t + 2); + }, + easeInSine: function (t) { + return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1; + }, + easeOutSine: function (t) { + return 1 * Math.sin(t / 1 * (Math.PI / 2)); + }, + easeInOutSine: function (t) { + return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1); + }, + easeInExpo: function (t) { + return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1)); + }, + easeOutExpo: function (t) { + return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1); + }, + easeInOutExpo: function (t) { + if (t === 0) return 0; + if (t === 1) return 1; + if ((t /= 1 / 2) < 1) return 1 / 2 * Math.pow(2, 10 * (t - 1)); + return 1 / 2 * (-Math.pow(2, -10 * --t) + 2); + }, + easeInCirc: function (t) { + if (t >= 1) return t; + return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1); + }, + easeOutCirc: function (t) { + return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t); + }, + easeInOutCirc: function (t) { + if ((t /= 1 / 2) < 1) return -1 / 2 * (Math.sqrt(1 - t * t) - 1); + return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1); + }, + easeInElastic: function (t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) return 0; + if ((t /= 1) == 1) return 1; + if (!p) p = 1 * 0.3; + if (a < Math.abs(1)) { + a = 1; + s = p / 4; + } else s = p / (2 * Math.PI) * Math.asin(1 / a); + return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); + }, + easeOutElastic: function (t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) return 0; + if ((t /= 1) == 1) return 1; + if (!p) p = 1 * 0.3; + if (a < Math.abs(1)) { + a = 1; + s = p / 4; + } else s = p / (2 * Math.PI) * Math.asin(1 / a); + return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1; + }, + easeInOutElastic: function (t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) return 0; + if ((t /= 1 / 2) == 2) return 1; + if (!p) p = 1 * (0.3 * 1.5); + if (a < Math.abs(1)) { + a = 1; + s = p / 4; + } else s = p / (2 * Math.PI) * Math.asin(1 / a); + if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); + return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1; + }, + easeInBack: function (t) { + var s = 1.70158; + return 1 * (t /= 1) * t * ((s + 1) * t - s); + }, + easeOutBack: function (t) { + var s = 1.70158; + return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1); + }, + easeInOutBack: function (t) { + var s = 1.70158; + if ((t /= 1 / 2) < 1) return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)); + return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); + }, + easeInBounce: function (t) { + return 1 - easingEffects.easeOutBounce(1 - t); + }, + easeOutBounce: function (t) { + if ((t /= 1) < (1 / 2.75)) { + return 1 * (7.5625 * t * t); + } else if (t < (2 / 2.75)) { + return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75); + } else if (t < (2.5 / 2.75)) { + return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375); + } else { + return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375); + } + }, + easeInOutBounce: function (t) { + if (t < 1 / 2) return easingEffects.easeInBounce(t * 2) * 0.5; + return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5; + } + }, + //Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ + requestAnimFrame = helpers.requestAnimFrame = (function(){ + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback) { + return window.setTimeout(callback, 1000 / 60); + }; + })(), + cancelAnimFrame = helpers.cancelAnimFrame = (function(){ + return window.cancelAnimationFrame || + window.webkitCancelAnimationFrame || + window.mozCancelAnimationFrame || + window.oCancelAnimationFrame || + window.msCancelAnimationFrame || + function(callback) { + return window.clearTimeout(callback, 1000 / 60); + }; + })(), + animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){ + + var currentStep = 0, + easingFunction = easingEffects[easingString] || easingEffects.linear; + + var animationFrame = function(){ + currentStep++; + var stepDecimal = currentStep/totalSteps; + var easeDecimal = easingFunction(stepDecimal); + + callback.call(chartInstance,easeDecimal,stepDecimal, currentStep); + onProgress.call(chartInstance,easeDecimal,stepDecimal); + if (currentStep < totalSteps){ + chartInstance.animationFrame = requestAnimFrame(animationFrame); + } else{ + onComplete.apply(chartInstance); + } + }; + requestAnimFrame(animationFrame); + }, + //-- DOM methods + getRelativePosition = helpers.getRelativePosition = function(evt){ + var mouseX, mouseY; + var e = evt.originalEvent || evt, + canvas = evt.currentTarget || evt.srcElement, + boundingRect = canvas.getBoundingClientRect(); + + if (e.touches){ + mouseX = e.touches[0].clientX - boundingRect.left; + mouseY = e.touches[0].clientY - boundingRect.top; + + } + else{ + mouseX = e.clientX - boundingRect.left; + mouseY = e.clientY - boundingRect.top; + } + + return { + x : mouseX, + y : mouseY + }; + + }, + addEvent = helpers.addEvent = function(node,eventType,method){ + if (node.addEventListener){ + node.addEventListener(eventType,method); + } else if (node.attachEvent){ + node.attachEvent("on"+eventType, method); + } else { + node["on"+eventType] = method; + } + }, + removeEvent = helpers.removeEvent = function(node, eventType, handler){ + if (node.removeEventListener){ + node.removeEventListener(eventType, handler, false); + } else if (node.detachEvent){ + node.detachEvent("on"+eventType,handler); + } else{ + node["on" + eventType] = noop; + } + }, + bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){ + // Create the events object if it's not already present + if (!chartInstance.events) chartInstance.events = {}; + + each(arrayOfEvents,function(eventName){ + chartInstance.events[eventName] = function(){ + handler.apply(chartInstance, arguments); + }; + addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]); + }); + }, + unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) { + each(arrayOfEvents, function(handler,eventName){ + removeEvent(chartInstance.chart.canvas, eventName, handler); + }); + }, + getMaximumWidth = helpers.getMaximumWidth = function(domNode){ + var container = domNode.parentNode; + // TODO = check cross browser stuff with this. + return container.clientWidth; + }, + getMaximumHeight = helpers.getMaximumHeight = function(domNode){ + var container = domNode.parentNode; + // TODO = check cross browser stuff with this. + return container.clientHeight; + }, + getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support + retinaScale = helpers.retinaScale = function(chart){ + var ctx = chart.ctx, + width = chart.canvas.width, + height = chart.canvas.height; + + if (window.devicePixelRatio) { + ctx.canvas.style.width = width + "px"; + ctx.canvas.style.height = height + "px"; + ctx.canvas.height = height * window.devicePixelRatio; + ctx.canvas.width = width * window.devicePixelRatio; + ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + } + }, + //-- Canvas methods + clear = helpers.clear = function(chart){ + chart.ctx.clearRect(0,0,chart.width,chart.height); + }, + fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){ + return fontStyle + " " + pixelSize+"px " + fontFamily; + }, + longestText = helpers.longestText = function(ctx,font,arrayOfStrings){ + ctx.font = font; + var longest = 0; + each(arrayOfStrings,function(string){ + var textWidth = ctx.measureText(string).width; + longest = (textWidth > longest) ? textWidth : longest; + }); + return longest; + }, + drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){ + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); + }; + + + //Store a reference to each instance - allowing us to globally resize chart instances on window resize. + //Destroy method on the chart will remove the instance of the chart from this reference. + Chart.instances = {}; + + Chart.Type = function(data,options,chart){ + this.options = options; + this.chart = chart; + this.id = uid(); + //Add the chart instance to the global namespace + Chart.instances[this.id] = this; + + // Initialize is always called when a chart type is created + // By default it is a no op, but it should be extended + if (options.responsive){ + this.resize(); + } + this.initialize.call(this,data); + }; + + //Core methods that'll be a part of every chart type + extend(Chart.Type.prototype,{ + initialize : function(){return this;}, + clear : function(){ + clear(this.chart); + return this; + }, + stop : function(){ + // Stops any current animation loop occuring + cancelAnimFrame(this.animationFrame); + return this; + }, + resize : function(callback){ + this.stop(); + var canvas = this.chart.canvas, + newWidth = getMaximumWidth(this.chart.canvas), + newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas); + + canvas.width = this.chart.width = newWidth; + canvas.height = this.chart.height = newHeight; + + retinaScale(this.chart); + + if (typeof callback === "function"){ + callback.apply(this, Array.prototype.slice.call(arguments, 1)); + } + return this; + }, + reflow : noop, + render : function(reflow){ + if (reflow){ + this.reflow(); + } + if (this.options.animation && !reflow){ + helpers.animationLoop( + this.draw, + this.options.animationSteps, + this.options.animationEasing, + this.options.onAnimationProgress, + this.options.onAnimationComplete, + this + ); + } + else{ + this.draw(); + this.options.onAnimationComplete.call(this); + } + return this; + }, + generateLegend : function(){ + return template(this.options.legendTemplate,this); + }, + destroy : function(){ + this.clear(); + unbindEvents(this, this.events); + var canvas = this.chart.canvas; + + // Reset canvas height/width attributes starts a fresh with the canvas context + canvas.width = this.chart.width; + canvas.height = this.chart.height; + + // < IE9 doesn't support removeProperty + if (canvas.style.removeProperty) { + canvas.style.removeProperty('width'); + canvas.style.removeProperty('height'); + } else { + canvas.style.removeAttribute('width'); + canvas.style.removeAttribute('height'); + } + + delete Chart.instances[this.id]; + }, + showTooltip : function(ChartElements, forceRedraw){ + // Only redraw the chart if we've actually changed what we're hovering on. + if (typeof this.activeElements === 'undefined') this.activeElements = []; + + var isChanged = (function(Elements){ + var changed = false; + + if (Elements.length !== this.activeElements.length){ + changed = true; + return changed; + } + + each(Elements, function(element, index){ + if (element !== this.activeElements[index]){ + changed = true; + } + }, this); + return changed; + }).call(this, ChartElements); + + if (!isChanged && !forceRedraw){ + return; + } + else{ + this.activeElements = ChartElements; + } + this.draw(); + if(this.options.customTooltips){ + this.options.customTooltips(false); + } + if (ChartElements.length > 0){ + // If we have multiple datasets, show a MultiTooltip for all of the data points at that index + if (this.datasets && this.datasets.length > 1) { + var dataArray, + dataIndex; + + for (var i = this.datasets.length - 1; i >= 0; i--) { + dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments; + dataIndex = indexOf(dataArray, ChartElements[0]); + if (dataIndex !== -1){ + break; + } + } + var tooltipLabels = [], + tooltipColors = [], + medianPosition = (function(index) { + + // Get all the points at that particular index + var Elements = [], + dataCollection, + xPositions = [], + yPositions = [], + xMax, + yMax, + xMin, + yMin; + helpers.each(this.datasets, function(dataset){ + dataCollection = dataset.points || dataset.bars || dataset.segments; + if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){ + Elements.push(dataCollection[dataIndex]); + } + }); + + helpers.each(Elements, function(element) { + xPositions.push(element.x); + yPositions.push(element.y); + + + //Include any colour information about the element + tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element)); + tooltipColors.push({ + fill: element._saved.fillColor || element.fillColor, + stroke: element._saved.strokeColor || element.strokeColor + }); + + }, this); + + yMin = min(yPositions); + yMax = max(yPositions); + + xMin = min(xPositions); + xMax = max(xPositions); + + return { + x: (xMin > this.chart.width/2) ? xMin : xMax, + y: (yMin + yMax)/2 + }; + }).call(this, dataIndex); + + new Chart.MultiTooltip({ + x: medianPosition.x, + y: medianPosition.y, + xPadding: this.options.tooltipXPadding, + yPadding: this.options.tooltipYPadding, + xOffset: this.options.tooltipXOffset, + fillColor: this.options.tooltipFillColor, + textColor: this.options.tooltipFontColor, + fontFamily: this.options.tooltipFontFamily, + fontStyle: this.options.tooltipFontStyle, + fontSize: this.options.tooltipFontSize, + titleTextColor: this.options.tooltipTitleFontColor, + titleFontFamily: this.options.tooltipTitleFontFamily, + titleFontStyle: this.options.tooltipTitleFontStyle, + titleFontSize: this.options.tooltipTitleFontSize, + cornerRadius: this.options.tooltipCornerRadius, + labels: tooltipLabels, + legendColors: tooltipColors, + legendColorBackground : this.options.multiTooltipKeyBackground, + title: ChartElements[0].label, + chart: this.chart, + ctx: this.chart.ctx, + custom: this.options.customTooltips + }).draw(); + + } else { + each(ChartElements, function(Element) { + var tooltipPosition = Element.tooltipPosition(); + new Chart.Tooltip({ + x: Math.round(tooltipPosition.x), + y: Math.round(tooltipPosition.y), + xPadding: this.options.tooltipXPadding, + yPadding: this.options.tooltipYPadding, + fillColor: this.options.tooltipFillColor, + textColor: this.options.tooltipFontColor, + fontFamily: this.options.tooltipFontFamily, + fontStyle: this.options.tooltipFontStyle, + fontSize: this.options.tooltipFontSize, + caretHeight: this.options.tooltipCaretSize, + cornerRadius: this.options.tooltipCornerRadius, + text: template(this.options.tooltipTemplate, Element), + chart: this.chart, + custom: this.options.customTooltips + }).draw(); + }, this); + } + } + return this; + }, + toBase64Image : function(){ + return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments); + } + }); + + Chart.Type.extend = function(extensions){ + + var parent = this; + + var ChartType = function(){ + return parent.apply(this,arguments); + }; + + //Copy the prototype object of the this class + ChartType.prototype = clone(parent.prototype); + //Now overwrite some of the properties in the base class with the new extensions + extend(ChartType.prototype, extensions); + + ChartType.extend = Chart.Type.extend; + + if (extensions.name || parent.prototype.name){ + + var chartName = extensions.name || parent.prototype.name; + //Assign any potential default values of the new chart type + + //If none are defined, we'll use a clone of the chart type this is being extended from. + //I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart + //doesn't define some defaults of their own. + + var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {}; + + Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults); + + Chart.types[chartName] = ChartType; + + //Register this new chart type in the Chart prototype + Chart.prototype[chartName] = function(data,options){ + var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {}); + return new ChartType(data,config,this); + }; + } else{ + warn("Name not provided for this chart, so it hasn't been registered"); + } + return parent; + }; + + Chart.Element = function(configuration){ + extend(this,configuration); + this.initialize.apply(this,arguments); + this.save(); + }; + extend(Chart.Element.prototype,{ + initialize : function(){}, + restore : function(props){ + if (!props){ + extend(this,this._saved); + } else { + each(props,function(key){ + this[key] = this._saved[key]; + },this); + } + return this; + }, + save : function(){ + this._saved = clone(this); + delete this._saved._saved; + return this; + }, + update : function(newProps){ + each(newProps,function(value,key){ + this._saved[key] = this[key]; + this[key] = value; + },this); + return this; + }, + transition : function(props,ease){ + each(props,function(value,key){ + this[key] = ((value - this._saved[key]) * ease) + this._saved[key]; + },this); + return this; + }, + tooltipPosition : function(){ + return { + x : this.x, + y : this.y + }; + }, + hasValue: function(){ + return isNumber(this.value); + } + }); + + Chart.Element.extend = inherits; + + + Chart.Point = Chart.Element.extend({ + display: true, + inRange: function(chartX,chartY){ + var hitDetectionRange = this.hitDetectionRadius + this.radius; + return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2)); + }, + draw : function(){ + if (this.display){ + var ctx = this.ctx; + ctx.beginPath(); + + ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2); + ctx.closePath(); + + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + ctx.fillStyle = this.fillColor; + + ctx.fill(); + ctx.stroke(); + } + + + //Quick debug for bezier curve splining + //Highlights control points and the line between them. + //Handy for dev - stripped in the min version. + + // ctx.save(); + // ctx.fillStyle = "black"; + // ctx.strokeStyle = "black" + // ctx.beginPath(); + // ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2); + // ctx.fill(); + + // ctx.beginPath(); + // ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2); + // ctx.fill(); + + // ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y); + // ctx.lineTo(this.x, this.y); + // ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y); + // ctx.stroke(); + + // ctx.restore(); + + + + } + }); + + Chart.Arc = Chart.Element.extend({ + inRange : function(chartX,chartY){ + + var pointRelativePosition = helpers.getAngleFromPoint(this, { + x: chartX, + y: chartY + }); + + //Check if within the range of the open/close angle + var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle), + withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius); + + return (betweenAngles && withinRadius); + //Ensure within the outside of the arc centre, but inside arc outer + }, + tooltipPosition : function(){ + var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2), + rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius; + return { + x : this.x + (Math.cos(centreAngle) * rangeFromCentre), + y : this.y + (Math.sin(centreAngle) * rangeFromCentre) + }; + }, + draw : function(animationPercent){ + + var easingDecimal = animationPercent || 1; + + var ctx = this.ctx; + + ctx.beginPath(); + + ctx.arc(this.x, this.y, this.outerRadius, this.startAngle, this.endAngle); + + ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true); + + ctx.closePath(); + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + ctx.fillStyle = this.fillColor; + + ctx.fill(); + ctx.lineJoin = 'bevel'; + + if (this.showStroke){ + ctx.stroke(); + } + } + }); + + Chart.Rectangle = Chart.Element.extend({ + draw : function(){ + var ctx = this.ctx, + halfWidth = this.width/2, + leftX = this.x - halfWidth, + rightX = this.x + halfWidth, + top = this.base - (this.base - this.y), + halfStroke = this.strokeWidth / 2; + + // Canvas doesn't allow us to stroke inside the width so we can + // adjust the sizes to fit if we're setting a stroke on the line + if (this.showStroke){ + leftX += halfStroke; + rightX -= halfStroke; + top += halfStroke; + } + + ctx.beginPath(); + + ctx.fillStyle = this.fillColor; + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + // It'd be nice to keep this class totally generic to any rectangle + // and simply specify which border to miss out. + ctx.moveTo(leftX, this.base); + ctx.lineTo(leftX, top); + ctx.lineTo(rightX, top); + ctx.lineTo(rightX, this.base); + ctx.fill(); + if (this.showStroke){ + ctx.stroke(); + } + }, + height : function(){ + return this.base - this.y; + }, + inRange : function(chartX,chartY){ + return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base); + } + }); + + Chart.Tooltip = Chart.Element.extend({ + draw : function(){ + + var ctx = this.chart.ctx; + + ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); + + this.xAlign = "center"; + this.yAlign = "above"; + + //Distance between the actual element.y position and the start of the tooltip caret + var caretPadding = this.caretPadding = 2; + + var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding, + tooltipRectHeight = this.fontSize + 2*this.yPadding, + tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding; + + if (this.x + tooltipWidth/2 >this.chart.width){ + this.xAlign = "left"; + } else if (this.x - tooltipWidth/2 < 0){ + this.xAlign = "right"; + } + + if (this.y - tooltipHeight < 0){ + this.yAlign = "below"; + } + + + var tooltipX = this.x - tooltipWidth/2, + tooltipY = this.y - tooltipHeight; + + ctx.fillStyle = this.fillColor; + + // Custom Tooltips + if(this.custom){ + this.custom(this); + } + else{ + switch(this.yAlign) + { + case "above": + //Draw a caret above the x/y + ctx.beginPath(); + ctx.moveTo(this.x,this.y - caretPadding); + ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight)); + ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight)); + ctx.closePath(); + ctx.fill(); + break; + case "below": + tooltipY = this.y + caretPadding + this.caretHeight; + //Draw a caret below the x/y + ctx.beginPath(); + ctx.moveTo(this.x, this.y + caretPadding); + ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight); + ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight); + ctx.closePath(); + ctx.fill(); + break; + } + + switch(this.xAlign) + { + case "left": + tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight); + break; + case "right": + tooltipX = this.x - (this.cornerRadius + this.caretHeight); + break; + } + + drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius); + + ctx.fill(); + + ctx.fillStyle = this.textColor; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2); + } + } + }); + + Chart.MultiTooltip = Chart.Element.extend({ + initialize : function(){ + this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); + + this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily); + + this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleFontSize *1.5; + + this.ctx.font = this.titleFont; + + var titleWidth = this.ctx.measureText(this.title).width, + //Label has a legend square as well so account for this. + labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3, + longestTextWidth = max([labelWidth,titleWidth]); + + this.width = longestTextWidth + (this.xPadding*2); + + + var halfHeight = this.height/2; + + //Check to ensure the height will fit on the canvas + if (this.y - halfHeight < 0 ){ + this.y = halfHeight; + } else if (this.y + halfHeight > this.chart.height){ + this.y = this.chart.height - halfHeight; + } + + //Decide whether to align left or right based on position on canvas + if (this.x > this.chart.width/2){ + this.x -= this.xOffset + this.width; + } else { + this.x += this.xOffset; + } + + + }, + getLineHeight : function(index){ + var baseLineHeight = this.y - (this.height/2) + this.yPadding, + afterTitleIndex = index-1; + + //If the index is zero, we're getting the title + if (index === 0){ + return baseLineHeight + this.titleFontSize/2; + } else{ + return baseLineHeight + ((this.fontSize*1.5*afterTitleIndex) + this.fontSize/2) + this.titleFontSize * 1.5; + } + + }, + draw : function(){ + // Custom Tooltips + if(this.custom){ + this.custom(this); + } + else{ + drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius); + var ctx = this.ctx; + ctx.fillStyle = this.fillColor; + ctx.fill(); + ctx.closePath(); + + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.fillStyle = this.titleTextColor; + ctx.font = this.titleFont; + + ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0)); + + ctx.font = this.font; + helpers.each(this.labels,function(label,index){ + ctx.fillStyle = this.textColor; + ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1)); + + //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas) + //ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + //Instead we'll make a white filled block to put the legendColour palette over. + + ctx.fillStyle = this.legendColorBackground; + ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + + ctx.fillStyle = this.legendColors[index].fill; + ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + + + },this); + } + } + }); + + Chart.Scale = Chart.Element.extend({ + initialize : function(){ + this.fit(); + }, + buildYLabels : function(){ + this.yLabels = []; + + var stepDecimalPlaces = getDecimalPlaces(this.stepValue); + + for (var i=0; i<=this.steps; i++){ + this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); + } + this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) : 0; + }, + addXLabel : function(label){ + this.xLabels.push(label); + this.valuesCount++; + this.fit(); + }, + removeXLabel : function(){ + this.xLabels.shift(); + this.valuesCount--; + this.fit(); + }, + // Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use + fit: function(){ + // First we need the width of the yLabels, assuming the xLabels aren't rotated + + // To do that we need the base line at the top and base of the chart, assuming there is no x label rotation + this.startPoint = (this.display) ? this.fontSize : 0; + this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels + + // Apply padding settings to the start and end point. + this.startPoint += this.padding; + this.endPoint -= this.padding; + + // Cache the starting height, so can determine if we need to recalculate the scale yAxis + var cachedHeight = this.endPoint - this.startPoint, + cachedYLabelWidth; + + // Build the current yLabels so we have an idea of what size they'll be to start + /* + * This sets what is returned from calculateScaleRange as static properties of this class: + * + this.steps; + this.stepValue; + this.min; + this.max; + * + */ + this.calculateYRange(cachedHeight); + + // With these properties set we can now build the array of yLabels + // and also the width of the largest yLabel + this.buildYLabels(); + + this.calculateXLabelRotation(); + + while((cachedHeight > this.endPoint - this.startPoint)){ + cachedHeight = this.endPoint - this.startPoint; + cachedYLabelWidth = this.yLabelWidth; + + this.calculateYRange(cachedHeight); + this.buildYLabels(); + + // Only go through the xLabel loop again if the yLabel width has changed + if (cachedYLabelWidth < this.yLabelWidth){ + this.calculateXLabelRotation(); + } + } + + }, + calculateXLabelRotation : function(){ + //Get the width of each grid by calculating the difference + //between x offsets between 0 and 1. + + this.ctx.font = this.font; + + var firstWidth = this.ctx.measureText(this.xLabels[0]).width, + lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width, + firstRotated, + lastRotated; + + + this.xScalePaddingRight = lastWidth/2 + 3; + this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth + 10) ? firstWidth/2 : this.yLabelWidth + 10; + + this.xLabelRotation = 0; + if (this.display){ + var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels), + cosRotation, + firstRotatedWidth; + this.xLabelWidth = originalLabelWidth; + //Allow 3 pixels x2 padding either side for label readability + var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6; + + //Max label rotate should be 90 - also act as a loop counter + while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){ + cosRotation = Math.cos(toRadians(this.xLabelRotation)); + + firstRotated = cosRotation * firstWidth; + lastRotated = cosRotation * lastWidth; + + // We're right aligning the text now. + if (firstRotated + this.fontSize / 2 > this.yLabelWidth + 8){ + this.xScalePaddingLeft = firstRotated + this.fontSize / 2; + } + this.xScalePaddingRight = this.fontSize/2; + + + this.xLabelRotation++; + this.xLabelWidth = cosRotation * originalLabelWidth; + + } + if (this.xLabelRotation > 0){ + this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3; + } + } + else{ + this.xLabelWidth = 0; + this.xScalePaddingRight = this.padding; + this.xScalePaddingLeft = this.padding; + } + + }, + // Needs to be overidden in each Chart type + // Otherwise we need to pass all the data into the scale class + calculateYRange: noop, + drawingArea: function(){ + return this.startPoint - this.endPoint; + }, + calculateY : function(value){ + var scalingFactor = this.drawingArea() / (this.min - this.max); + return this.endPoint - (scalingFactor * (value - this.min)); + }, + calculateX : function(index){ + var isRotated = (this.xLabelRotation > 0), + // innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding, + innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight), + valueWidth = innerWidth/Math.max((this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), 1), + valueOffset = (valueWidth * index) + this.xScalePaddingLeft; + + if (this.offsetGridLines){ + valueOffset += (valueWidth/2); + } + + return Math.round(valueOffset); + }, + update : function(newProps){ + helpers.extend(this, newProps); + this.fit(); + }, + draw : function(){ + var ctx = this.ctx, + yLabelGap = (this.endPoint - this.startPoint) / this.steps, + xStart = Math.round(this.xScalePaddingLeft); + if (this.display){ + ctx.fillStyle = this.textColor; + ctx.font = this.font; + each(this.yLabels,function(labelString,index){ + var yLabelCenter = this.endPoint - (yLabelGap * index), + linePositionY = Math.round(yLabelCenter), + drawHorizontalLine = this.showHorizontalLines; + + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + if (this.showLabels){ + ctx.fillText(labelString,xStart - 10,yLabelCenter); + } + + // This is X axis, so draw it + if (index === 0 && !drawHorizontalLine){ + drawHorizontalLine = true; + } + + if (drawHorizontalLine){ + ctx.beginPath(); + } + + if (index > 0){ + // This is a grid line in the centre, so drop that + ctx.lineWidth = this.gridLineWidth; + ctx.strokeStyle = this.gridLineColor; + } else { + // This is the first line on the scale + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + } + + linePositionY += helpers.aliasPixel(ctx.lineWidth); + + if(drawHorizontalLine){ + ctx.moveTo(xStart, linePositionY); + ctx.lineTo(this.width, linePositionY); + ctx.stroke(); + ctx.closePath(); + } + + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + ctx.beginPath(); + ctx.moveTo(xStart - 5, linePositionY); + ctx.lineTo(xStart, linePositionY); + ctx.stroke(); + ctx.closePath(); + + },this); + + each(this.xLabels,function(label,index){ + var xPos = this.calculateX(index) + aliasPixel(this.lineWidth), + // Check to see if line/bar here and decide where to place the line + linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth), + isRotated = (this.xLabelRotation > 0), + drawVerticalLine = this.showVerticalLines; + + // This is Y axis, so draw it + if (index === 0 && !drawVerticalLine){ + drawVerticalLine = true; + } + + if (drawVerticalLine){ + ctx.beginPath(); + } + + if (index > 0){ + // This is a grid line in the centre, so drop that + ctx.lineWidth = this.gridLineWidth; + ctx.strokeStyle = this.gridLineColor; + } else { + // This is the first line on the scale + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + } + + if (drawVerticalLine){ + ctx.moveTo(linePos,this.endPoint); + ctx.lineTo(linePos,this.startPoint - 3); + ctx.stroke(); + ctx.closePath(); + } + + + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + + + // Small lines at the bottom of the base grid line + ctx.beginPath(); + ctx.moveTo(linePos,this.endPoint); + ctx.lineTo(linePos,this.endPoint + 5); + ctx.stroke(); + ctx.closePath(); + + ctx.save(); + ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8); + ctx.rotate(toRadians(this.xLabelRotation)*-1); + ctx.font = this.font; + ctx.textAlign = (isRotated) ? "right" : "center"; + ctx.textBaseline = (isRotated) ? "middle" : "top"; + ctx.fillText(label, 0, 0); + ctx.restore(); + },this); + + } + } + + }); + + Chart.RadialScale = Chart.Element.extend({ + initialize: function(){ + this.size = min([this.height, this.width]); + this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); + }, + calculateCenterOffset: function(value){ + // Take into account half font size + the yPadding of the top value + var scalingFactor = this.drawingArea / (this.max - this.min); + + return (value - this.min) * scalingFactor; + }, + update : function(){ + if (!this.lineArc){ + this.setScaleSize(); + } else { + this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); + } + this.buildYLabels(); + }, + buildYLabels: function(){ + this.yLabels = []; + + var stepDecimalPlaces = getDecimalPlaces(this.stepValue); + + for (var i=0; i<=this.steps; i++){ + this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); + } + }, + getCircumference : function(){ + return ((Math.PI*2) / this.valuesCount); + }, + setScaleSize: function(){ + /* + * Right, this is really confusing and there is a lot of maths going on here + * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 + * + * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif + * + * Solution: + * + * We assume the radius of the polygon is half the size of the canvas at first + * at each index we check if the text overlaps. + * + * Where it does, we store that angle and that index. + * + * After finding the largest index and angle we calculate how much we need to remove + * from the shape radius to move the point inwards by that x. + * + * We average the left and right distances to get the maximum shape radius that can fit in the box + * along with labels. + * + * Once we have that, we can find the centre point for the chart, by taking the x text protrusion + * on each side, removing that from the size, halving it and adding the left x protrusion width. + * + * This will mean we have a shape fitted to the canvas, as large as it can be with the labels + * and position it in the most space efficient manner + * + * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif + */ + + + // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. + // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points + var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]), + pointPosition, + i, + textWidth, + halfTextWidth, + furthestRight = this.width, + furthestRightIndex, + furthestRightAngle, + furthestLeft = 0, + furthestLeftIndex, + furthestLeftAngle, + xProtrusionLeft, + xProtrusionRight, + radiusReductionRight, + radiusReductionLeft, + maxWidthRadius; + this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); + for (i=0;i furthestRight) { + furthestRight = pointPosition.x + halfTextWidth; + furthestRightIndex = i; + } + if (pointPosition.x - halfTextWidth < furthestLeft) { + furthestLeft = pointPosition.x - halfTextWidth; + furthestLeftIndex = i; + } + } + else if (i < this.valuesCount/2) { + // Less than half the values means we'll left align the text + if (pointPosition.x + textWidth > furthestRight) { + furthestRight = pointPosition.x + textWidth; + furthestRightIndex = i; + } + } + else if (i > this.valuesCount/2){ + // More than half the values means we'll right align the text + if (pointPosition.x - textWidth < furthestLeft) { + furthestLeft = pointPosition.x - textWidth; + furthestLeftIndex = i; + } + } + } + + xProtrusionLeft = furthestLeft; + + xProtrusionRight = Math.ceil(furthestRight - this.width); + + furthestRightAngle = this.getIndexAngle(furthestRightIndex); + + furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); + + radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2); + + radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2); + + // Ensure we actually need to reduce the size of the chart + radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0; + radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; + + this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2; + + //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2]) + this.setCenterPoint(radiusReductionLeft, radiusReductionRight); + + }, + setCenterPoint: function(leftMovement, rightMovement){ + + var maxRight = this.width - rightMovement - this.drawingArea, + maxLeft = leftMovement + this.drawingArea; + + this.xCenter = (maxLeft + maxRight)/2; + // Always vertically in the centre as the text height doesn't change + this.yCenter = (this.height/2); + }, + + getIndexAngle : function(index){ + var angleMultiplier = (Math.PI * 2) / this.valuesCount; + // Start from the top instead of right, so remove a quarter of the circle + + return index * angleMultiplier - (Math.PI/2); + }, + getPointPosition : function(index, distanceFromCenter){ + var thisAngle = this.getIndexAngle(index); + return { + x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter, + y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter + }; + }, + draw: function(){ + if (this.display){ + var ctx = this.ctx; + each(this.yLabels, function(label, index){ + // Don't draw a centre value + if (index > 0){ + var yCenterOffset = index * (this.drawingArea/this.steps), + yHeight = this.yCenter - yCenterOffset, + pointPosition; + + // Draw circular lines around the scale + if (this.lineWidth > 0){ + ctx.strokeStyle = this.lineColor; + ctx.lineWidth = this.lineWidth; + + if(this.lineArc){ + ctx.beginPath(); + ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2); + ctx.closePath(); + ctx.stroke(); + } else{ + ctx.beginPath(); + for (var i=0;i= 0; i--) { + if (this.angleLineWidth > 0){ + var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max)); + ctx.beginPath(); + ctx.moveTo(this.xCenter, this.yCenter); + ctx.lineTo(outerPosition.x, outerPosition.y); + ctx.stroke(); + ctx.closePath(); + } + // Extra 3px out for some label spacing + var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5); + ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); + ctx.fillStyle = this.pointLabelFontColor; + + var labelsCount = this.labels.length, + halfLabelsCount = this.labels.length/2, + quarterLabelsCount = halfLabelsCount/2, + upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount), + exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount); + if (i === 0){ + ctx.textAlign = 'center'; + } else if(i === halfLabelsCount){ + ctx.textAlign = 'center'; + } else if (i < halfLabelsCount){ + ctx.textAlign = 'left'; + } else { + ctx.textAlign = 'right'; + } + + // Set the correct text baseline based on outer positioning + if (exactQuarter){ + ctx.textBaseline = 'middle'; + } else if (upperHalf){ + ctx.textBaseline = 'bottom'; + } else { + ctx.textBaseline = 'top'; + } + + ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y); + } + } + } + } + }); + + // Attach global event to resize each chart instance when the browser resizes + helpers.addEvent(window, "resize", (function(){ + // Basic debounce of resize function so it doesn't hurt performance when resizing browser. + var timeout; + return function(){ + clearTimeout(timeout); + timeout = setTimeout(function(){ + each(Chart.instances,function(instance){ + // If the responsive flag is set in the chart instance config + // Cascade the resize event down to the chart. + if (instance.options.responsive){ + instance.resize(instance.render, true); + } + }); + }, 50); + }; + })()); + + + if (amd) { + define(function(){ + return Chart; + }); + } else if (typeof module === 'object' && module.exports) { + module.exports = Chart; + } + + root.Chart = Chart; + + Chart.noConflict = function(){ + root.Chart = previous; + return Chart; + }; + +}).call(this); + +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + + var defaultConfig = { + //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value + scaleBeginAtZero : true, + + //Boolean - Whether grid lines are shown across the chart + scaleShowGridLines : true, + + //String - Colour of the grid lines + scaleGridLineColor : "rgba(0,0,0,.05)", + + //Number - Width of the grid lines + scaleGridLineWidth : 1, + + //Boolean - Whether to show horizontal lines (except X axis) + scaleShowHorizontalLines: true, + + //Boolean - Whether to show vertical lines (except Y axis) + scaleShowVerticalLines: true, + + //Boolean - If there is a stroke on each bar + barShowStroke : true, + + //Number - Pixel width of the bar stroke + barStrokeWidth : 2, + + //Number - Spacing between each of the X value sets + barValueSpacing : 5, + + //Number - Spacing between data sets within X values + barDatasetSpacing : 1, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" + + }; + + + Chart.Type.extend({ + name: "Bar", + defaults : defaultConfig, + initialize: function(data){ + + //Expose options as a scope variable here so we can access it in the ScaleClass + var options = this.options; + + this.ScaleClass = Chart.Scale.extend({ + offsetGridLines : true, + calculateBarX : function(datasetCount, datasetIndex, barIndex){ + //Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar + var xWidth = this.calculateBaseWidth(), + xAbsolute = this.calculateX(barIndex) - (xWidth/2), + barWidth = this.calculateBarWidth(datasetCount); + + return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2; + }, + calculateBaseWidth : function(){ + return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing); + }, + calculateBarWidth : function(datasetCount){ + //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset + var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing); + + return (baseWidth / datasetCount); + } + }); + + this.datasets = []; + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : []; + + this.eachBars(function(bar){ + bar.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activeBars, function(activeBar){ + activeBar.fillColor = activeBar.highlightFill; + activeBar.strokeColor = activeBar.highlightStroke; + }); + this.showTooltip(activeBars); + }); + } + + //Declare the extension of the default point, to cater for the options passed in to the constructor + this.BarClass = Chart.Rectangle.extend({ + strokeWidth : this.options.barStrokeWidth, + showStroke : this.options.barShowStroke, + ctx : this.chart.ctx + }); + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset,datasetIndex){ + + var datasetObject = { + label : dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + bars : [] + }; + + this.datasets.push(datasetObject); + + helpers.each(dataset.data,function(dataPoint,index){ + //Add a new point for each piece of data, passing any required data to draw. + datasetObject.bars.push(new this.BarClass({ + value : dataPoint, + label : data.labels[index], + datasetLabel: dataset.label, + strokeColor : dataset.strokeColor, + fillColor : dataset.fillColor, + highlightFill : dataset.highlightFill || dataset.fillColor, + highlightStroke : dataset.highlightStroke || dataset.strokeColor + })); + },this); + + },this); + + this.buildScale(data.labels); + + this.BarClass.prototype.base = this.scale.endPoint; + + this.eachBars(function(bar, index, datasetIndex){ + helpers.extend(bar, { + width : this.scale.calculateBarWidth(this.datasets.length), + x: this.scale.calculateBarX(this.datasets.length, datasetIndex, index), + y: this.scale.endPoint + }); + bar.save(); + }, this); + + this.render(); + }, + update : function(){ + this.scale.update(); + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement){ + activeElement.restore(['fillColor', 'strokeColor']); + }); + + this.eachBars(function(bar){ + bar.save(); + }); + this.render(); + }, + eachBars : function(callback){ + helpers.each(this.datasets,function(dataset, datasetIndex){ + helpers.each(dataset.bars, callback, this, datasetIndex); + },this); + }, + getBarsAtEvent : function(e){ + var barsArray = [], + eventPosition = helpers.getRelativePosition(e), + datasetIterator = function(dataset){ + barsArray.push(dataset.bars[barIndex]); + }, + barIndex; + + for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) { + for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) { + if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){ + helpers.each(this.datasets, datasetIterator); + return barsArray; + } + } + } + + return barsArray; + }, + buildScale : function(labels){ + var self = this; + + var dataTotal = function(){ + var values = []; + self.eachBars(function(bar){ + values.push(bar.value); + }); + return values; + }; + + var scaleOptions = { + templateString : this.options.scaleLabel, + height : this.chart.height, + width : this.chart.width, + ctx : this.chart.ctx, + textColor : this.options.scaleFontColor, + fontSize : this.options.scaleFontSize, + fontStyle : this.options.scaleFontStyle, + fontFamily : this.options.scaleFontFamily, + valuesCount : labels.length, + beginAtZero : this.options.scaleBeginAtZero, + integersOnly : this.options.scaleIntegersOnly, + calculateYRange: function(currentHeight){ + var updatedRanges = helpers.calculateScaleRange( + dataTotal(), + currentHeight, + this.fontSize, + this.beginAtZero, + this.integersOnly + ); + helpers.extend(this, updatedRanges); + }, + xLabels : labels, + font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), + lineWidth : this.options.scaleLineWidth, + lineColor : this.options.scaleLineColor, + showHorizontalLines : this.options.scaleShowHorizontalLines, + showVerticalLines : this.options.scaleShowVerticalLines, + gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, + gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", + padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0, + showLabels : this.options.scaleShowLabels, + display : this.options.showScale + }; + + if (this.options.scaleOverride){ + helpers.extend(scaleOptions, { + calculateYRange: helpers.noop, + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + }); + } + + this.scale = new this.ScaleClass(scaleOptions); + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + helpers.each(valuesArray,function(value,datasetIndex){ + //Add a new point for each piece of data, passing any required data to draw. + this.datasets[datasetIndex].bars.push(new this.BarClass({ + value : value, + label : label, + x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1), + y: this.scale.endPoint, + width : this.scale.calculateBarWidth(this.datasets.length), + base : this.scale.endPoint, + strokeColor : this.datasets[datasetIndex].strokeColor, + fillColor : this.datasets[datasetIndex].fillColor + })); + },this); + + this.scale.addXLabel(label); + //Then re-render the chart. + this.update(); + }, + removeData : function(){ + this.scale.removeXLabel(); + //Then re-render the chart. + helpers.each(this.datasets,function(dataset){ + dataset.bars.shift(); + },this); + this.update(); + }, + reflow : function(){ + helpers.extend(this.BarClass.prototype,{ + y: this.scale.endPoint, + base : this.scale.endPoint + }); + var newScaleProps = helpers.extend({ + height : this.chart.height, + width : this.chart.width + }); + this.scale.update(newScaleProps); + }, + draw : function(ease){ + var easingDecimal = ease || 1; + this.clear(); + + var ctx = this.chart.ctx; + + this.scale.draw(easingDecimal); + + //Draw all the bars for each dataset + helpers.each(this.datasets,function(dataset,datasetIndex){ + helpers.each(dataset.bars,function(bar,index){ + if (bar.hasValue()){ + bar.base = this.scale.endPoint; + //Transition then draw + bar.transition({ + x : this.scale.calculateBarX(this.datasets.length, datasetIndex, index), + y : this.scale.calculateY(bar.value), + width : this.scale.calculateBarWidth(this.datasets.length) + }, easingDecimal).draw(); + } + },this); + + },this); + } + }); + + +}).call(this); + +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + //Cache a local reference to Chart.helpers + helpers = Chart.helpers; + + var defaultConfig = { + //Boolean - Whether we should show a stroke on each segment + segmentShowStroke : true, + + //String - The colour of each segment stroke + segmentStrokeColor : "#fff", + + //Number - The width of each segment stroke + segmentStrokeWidth : 2, + + //The percentage of the chart that we cut out of the middle. + percentageInnerCutout : 50, + + //Number - Amount of animation steps + animationSteps : 100, + + //String - Animation easing effect + animationEasing : "easeOutBounce", + + //Boolean - Whether we animate the rotation of the Doughnut + animateRotate : true, + + //Boolean - Whether we animate scaling the Doughnut from the centre + animateScale : false, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" + + }; + + + Chart.Type.extend({ + //Passing in a name registers this chart in the Chart namespace + name: "Doughnut", + //Providing a defaults will also register the deafults in the chart namespace + defaults : defaultConfig, + //Initialize is fired when the chart is initialized - Data is passed in as a parameter + //Config is automatically merged by the core of Chart.js, and is available at this.options + initialize: function(data){ + + //Declare segments as a static property to prevent inheriting across the Chart type prototype + this.segments = []; + this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; + + this.SegmentArc = Chart.Arc.extend({ + ctx : this.chart.ctx, + x : this.chart.width/2, + y : this.chart.height/2 + }); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; + + helpers.each(this.segments,function(segment){ + segment.restore(["fillColor"]); + }); + helpers.each(activeSegments,function(activeSegment){ + activeSegment.fillColor = activeSegment.highlightColor; + }); + this.showTooltip(activeSegments); + }); + } + this.calculateTotal(data); + + helpers.each(data,function(datapoint, index){ + this.addData(datapoint, index, true); + },this); + + this.render(); + }, + getSegmentsAtEvent : function(e){ + var segmentsArray = []; + + var location = helpers.getRelativePosition(e); + + helpers.each(this.segments,function(segment){ + if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); + },this); + return segmentsArray; + }, + addData : function(segment, atIndex, silent){ + var index = atIndex || this.segments.length; + this.segments.splice(index, 0, new this.SegmentArc({ + value : segment.value, + outerRadius : (this.options.animateScale) ? 0 : this.outerRadius, + innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout, + fillColor : segment.color, + highlightColor : segment.highlight || segment.color, + showStroke : this.options.segmentShowStroke, + strokeWidth : this.options.segmentStrokeWidth, + strokeColor : this.options.segmentStrokeColor, + startAngle : Math.PI * 1.5, + circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value), + label : segment.label + })); + if (!silent){ + this.reflow(); + this.update(); + } + }, + calculateCircumference : function(value){ + return (Math.PI*2)*(Math.abs(value) / this.total); + }, + calculateTotal : function(data){ + this.total = 0; + helpers.each(data,function(segment){ + this.total += Math.abs(segment.value); + },this); + }, + update : function(){ + this.calculateTotal(this.segments); + + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement){ + activeElement.restore(['fillColor']); + }); + + helpers.each(this.segments,function(segment){ + segment.save(); + }); + this.render(); + }, + + removeData: function(atIndex){ + var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; + this.segments.splice(indexToDelete, 1); + this.reflow(); + this.update(); + }, + + reflow : function(){ + helpers.extend(this.SegmentArc.prototype,{ + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; + helpers.each(this.segments, function(segment){ + segment.update({ + outerRadius : this.outerRadius, + innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout + }); + }, this); + }, + draw : function(easeDecimal){ + var animDecimal = (easeDecimal) ? easeDecimal : 1; + this.clear(); + helpers.each(this.segments,function(segment,index){ + segment.transition({ + circumference : this.calculateCircumference(segment.value), + outerRadius : this.outerRadius, + innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout + },animDecimal); + + segment.endAngle = segment.startAngle + segment.circumference; + + segment.draw(); + if (index === 0){ + segment.startAngle = Math.PI * 1.5; + } + //Check to see if it's the last segment, if not get the next and update the start angle + if (index < this.segments.length-1){ + this.segments[index+1].startAngle = segment.endAngle; + } + },this); + + } + }); + + Chart.types.Doughnut.extend({ + name : "Pie", + defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0}) + }); + +}).call(this); +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + var defaultConfig = { + + ///Boolean - Whether grid lines are shown across the chart + scaleShowGridLines : true, + + //String - Colour of the grid lines + scaleGridLineColor : "rgba(0,0,0,.05)", + + //Number - Width of the grid lines + scaleGridLineWidth : 1, + + //Boolean - Whether to show horizontal lines (except X axis) + scaleShowHorizontalLines: true, + + //Boolean - Whether to show vertical lines (except Y axis) + scaleShowVerticalLines: true, + + //Boolean - Whether the line is curved between points + bezierCurve : true, + + //Number - Tension of the bezier curve between points + bezierCurveTension : 0.4, + + //Boolean - Whether to show a dot for each point + pointDot : true, + + //Number - Radius of each point dot in pixels + pointDotRadius : 4, + + //Number - Pixel width of point dot stroke + pointDotStrokeWidth : 1, + + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHitDetectionRadius : 20, + + //Boolean - Whether to show a stroke for datasets + datasetStroke : true, + + //Number - Pixel width of dataset stroke + datasetStrokeWidth : 2, + + //Boolean - Whether to fill the dataset with a colour + datasetFill : true, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" + + }; + + + Chart.Type.extend({ + name: "Line", + defaults : defaultConfig, + initialize: function(data){ + //Declare the extension of the default point, to cater for the options passed in to the constructor + this.PointClass = Chart.Point.extend({ + strokeWidth : this.options.pointDotStrokeWidth, + radius : this.options.pointDotRadius, + display: this.options.pointDot, + hitDetectionRadius : this.options.pointHitDetectionRadius, + ctx : this.chart.ctx, + inRange : function(mouseX){ + return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2)); + } + }); + + this.datasets = []; + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; + this.eachPoints(function(point){ + point.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activePoints, function(activePoint){ + activePoint.fillColor = activePoint.highlightFill; + activePoint.strokeColor = activePoint.highlightStroke; + }); + this.showTooltip(activePoints); + }); + } + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset){ + + var datasetObject = { + label : dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + pointColor : dataset.pointColor, + pointStrokeColor : dataset.pointStrokeColor, + points : [] + }; + + this.datasets.push(datasetObject); + + + helpers.each(dataset.data,function(dataPoint,index){ + //Add a new point for each piece of data, passing any required data to draw. + datasetObject.points.push(new this.PointClass({ + value : dataPoint, + label : data.labels[index], + datasetLabel: dataset.label, + strokeColor : dataset.pointStrokeColor, + fillColor : dataset.pointColor, + highlightFill : dataset.pointHighlightFill || dataset.pointColor, + highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor + })); + },this); + + this.buildScale(data.labels); + + + this.eachPoints(function(point, index){ + helpers.extend(point, { + x: this.scale.calculateX(index), + y: this.scale.endPoint + }); + point.save(); + }, this); + + },this); + + + this.render(); + }, + update : function(){ + this.scale.update(); + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement){ + activeElement.restore(['fillColor', 'strokeColor']); + }); + this.eachPoints(function(point){ + point.save(); + }); + this.render(); + }, + eachPoints : function(callback){ + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,callback,this); + },this); + }, + getPointsAtEvent : function(e){ + var pointsArray = [], + eventPosition = helpers.getRelativePosition(e); + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,function(point){ + if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point); + }); + },this); + return pointsArray; + }, + buildScale : function(labels){ + var self = this; + + var dataTotal = function(){ + var values = []; + self.eachPoints(function(point){ + values.push(point.value); + }); + + return values; + }; + + var scaleOptions = { + templateString : this.options.scaleLabel, + height : this.chart.height, + width : this.chart.width, + ctx : this.chart.ctx, + textColor : this.options.scaleFontColor, + fontSize : this.options.scaleFontSize, + fontStyle : this.options.scaleFontStyle, + fontFamily : this.options.scaleFontFamily, + valuesCount : labels.length, + beginAtZero : this.options.scaleBeginAtZero, + integersOnly : this.options.scaleIntegersOnly, + calculateYRange : function(currentHeight){ + var updatedRanges = helpers.calculateScaleRange( + dataTotal(), + currentHeight, + this.fontSize, + this.beginAtZero, + this.integersOnly + ); + helpers.extend(this, updatedRanges); + }, + xLabels : labels, + font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), + lineWidth : this.options.scaleLineWidth, + lineColor : this.options.scaleLineColor, + showHorizontalLines : this.options.scaleShowHorizontalLines, + showVerticalLines : this.options.scaleShowVerticalLines, + gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, + gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", + padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth, + showLabels : this.options.scaleShowLabels, + display : this.options.showScale + }; + + if (this.options.scaleOverride){ + helpers.extend(scaleOptions, { + calculateYRange: helpers.noop, + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + }); + } + + + this.scale = new Chart.Scale(scaleOptions); + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + + helpers.each(valuesArray,function(value,datasetIndex){ + //Add a new point for each piece of data, passing any required data to draw. + this.datasets[datasetIndex].points.push(new this.PointClass({ + value : value, + label : label, + x: this.scale.calculateX(this.scale.valuesCount+1), + y: this.scale.endPoint, + strokeColor : this.datasets[datasetIndex].pointStrokeColor, + fillColor : this.datasets[datasetIndex].pointColor + })); + },this); + + this.scale.addXLabel(label); + //Then re-render the chart. + this.update(); + }, + removeData : function(){ + this.scale.removeXLabel(); + //Then re-render the chart. + helpers.each(this.datasets,function(dataset){ + dataset.points.shift(); + },this); + this.update(); + }, + reflow : function(){ + var newScaleProps = helpers.extend({ + height : this.chart.height, + width : this.chart.width + }); + this.scale.update(newScaleProps); + }, + draw : function(ease){ + var easingDecimal = ease || 1; + this.clear(); + + var ctx = this.chart.ctx; + + // Some helper methods for getting the next/prev points + var hasValue = function(item){ + return item.value !== null; + }, + nextPoint = function(point, collection, index){ + return helpers.findNextWhere(collection, hasValue, index) || point; + }, + previousPoint = function(point, collection, index){ + return helpers.findPreviousWhere(collection, hasValue, index) || point; + }; + + this.scale.draw(easingDecimal); + + + helpers.each(this.datasets,function(dataset){ + var pointsWithValues = helpers.where(dataset.points, hasValue); + + //Transition each point first so that the line and point drawing isn't out of sync + //We can use this extra loop to calculate the control points of this dataset also in this loop + + helpers.each(dataset.points, function(point, index){ + if (point.hasValue()){ + point.transition({ + y : this.scale.calculateY(point.value), + x : this.scale.calculateX(index) + }, easingDecimal); + } + },this); + + + // Control points need to be calculated in a seperate loop, because we need to know the current x/y of the point + // This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed + if (this.options.bezierCurve){ + helpers.each(pointsWithValues, function(point, index){ + var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0; + point.controlPoints = helpers.splineCurve( + previousPoint(point, pointsWithValues, index), + point, + nextPoint(point, pointsWithValues, index), + tension + ); + + // Prevent the bezier going outside of the bounds of the graph + + // Cap puter bezier handles to the upper/lower scale bounds + if (point.controlPoints.outer.y > this.scale.endPoint){ + point.controlPoints.outer.y = this.scale.endPoint; + } + else if (point.controlPoints.outer.y < this.scale.startPoint){ + point.controlPoints.outer.y = this.scale.startPoint; + } + + // Cap inner bezier handles to the upper/lower scale bounds + if (point.controlPoints.inner.y > this.scale.endPoint){ + point.controlPoints.inner.y = this.scale.endPoint; + } + else if (point.controlPoints.inner.y < this.scale.startPoint){ + point.controlPoints.inner.y = this.scale.startPoint; + } + },this); + } + + + //Draw the line between all the points + ctx.lineWidth = this.options.datasetStrokeWidth; + ctx.strokeStyle = dataset.strokeColor; + ctx.beginPath(); + + helpers.each(pointsWithValues, function(point, index){ + if (index === 0){ + ctx.moveTo(point.x, point.y); + } + else{ + if(this.options.bezierCurve){ + var previous = previousPoint(point, pointsWithValues, index); + + ctx.bezierCurveTo( + previous.controlPoints.outer.x, + previous.controlPoints.outer.y, + point.controlPoints.inner.x, + point.controlPoints.inner.y, + point.x, + point.y + ); + } + else{ + ctx.lineTo(point.x,point.y); + } + } + }, this); + + ctx.stroke(); + + if (this.options.datasetFill && pointsWithValues.length > 0){ + //Round off the line by going to the base of the chart, back to the start, then fill. + ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint); + ctx.lineTo(pointsWithValues[0].x, this.scale.endPoint); + ctx.fillStyle = dataset.fillColor; + ctx.closePath(); + ctx.fill(); + } + + //Now draw the points over the line + //A little inefficient double looping, but better than the line + //lagging behind the point positions + helpers.each(pointsWithValues,function(point){ + point.draw(); + }); + },this); + } + }); + + +}).call(this); + +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + //Cache a local reference to Chart.helpers + helpers = Chart.helpers; + + var defaultConfig = { + //Boolean - Show a backdrop to the scale label + scaleShowLabelBackdrop : true, + + //String - The colour of the label backdrop + scaleBackdropColor : "rgba(255,255,255,0.75)", + + // Boolean - Whether the scale should begin at zero + scaleBeginAtZero : true, + + //Number - The backdrop padding above & below the label in pixels + scaleBackdropPaddingY : 2, + + //Number - The backdrop padding to the side of the label in pixels + scaleBackdropPaddingX : 2, + + //Boolean - Show line for each value in the scale + scaleShowLine : true, + + //Boolean - Stroke a line around each segment in the chart + segmentShowStroke : true, + + //String - The colour of the stroke on each segement. + segmentStrokeColor : "#fff", + + //Number - The width of the stroke value in pixels + segmentStrokeWidth : 2, + + //Number - Amount of animation steps + animationSteps : 100, + + //String - Animation easing effect. + animationEasing : "easeOutBounce", + + //Boolean - Whether to animate the rotation of the chart + animateRotate : true, + + //Boolean - Whether to animate scaling the chart from the centre + animateScale : false, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" + }; + + + Chart.Type.extend({ + //Passing in a name registers this chart in the Chart namespace + name: "PolarArea", + //Providing a defaults will also register the deafults in the chart namespace + defaults : defaultConfig, + //Initialize is fired when the chart is initialized - Data is passed in as a parameter + //Config is automatically merged by the core of Chart.js, and is available at this.options + initialize: function(data){ + this.segments = []; + //Declare segment class as a chart instance specific class, so it can share props for this instance + this.SegmentArc = Chart.Arc.extend({ + showStroke : this.options.segmentShowStroke, + strokeWidth : this.options.segmentStrokeWidth, + strokeColor : this.options.segmentStrokeColor, + ctx : this.chart.ctx, + innerRadius : 0, + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.scale = new Chart.RadialScale({ + display: this.options.showScale, + fontStyle: this.options.scaleFontStyle, + fontSize: this.options.scaleFontSize, + fontFamily: this.options.scaleFontFamily, + fontColor: this.options.scaleFontColor, + showLabels: this.options.scaleShowLabels, + showLabelBackdrop: this.options.scaleShowLabelBackdrop, + backdropColor: this.options.scaleBackdropColor, + backdropPaddingY : this.options.scaleBackdropPaddingY, + backdropPaddingX: this.options.scaleBackdropPaddingX, + lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, + lineColor: this.options.scaleLineColor, + lineArc: true, + width: this.chart.width, + height: this.chart.height, + xCenter: this.chart.width/2, + yCenter: this.chart.height/2, + ctx : this.chart.ctx, + templateString: this.options.scaleLabel, + valuesCount: data.length + }); + + this.updateScaleRange(data); + + this.scale.update(); + + helpers.each(data,function(segment,index){ + this.addData(segment,index,true); + },this); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; + helpers.each(this.segments,function(segment){ + segment.restore(["fillColor"]); + }); + helpers.each(activeSegments,function(activeSegment){ + activeSegment.fillColor = activeSegment.highlightColor; + }); + this.showTooltip(activeSegments); + }); + } + + this.render(); + }, + getSegmentsAtEvent : function(e){ + var segmentsArray = []; + + var location = helpers.getRelativePosition(e); + + helpers.each(this.segments,function(segment){ + if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); + },this); + return segmentsArray; + }, + addData : function(segment, atIndex, silent){ + var index = atIndex || this.segments.length; + + this.segments.splice(index, 0, new this.SegmentArc({ + fillColor: segment.color, + highlightColor: segment.highlight || segment.color, + label: segment.label, + value: segment.value, + outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value), + circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(), + startAngle: Math.PI * 1.5 + })); + if (!silent){ + this.reflow(); + this.update(); + } + }, + removeData: function(atIndex){ + var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; + this.segments.splice(indexToDelete, 1); + this.reflow(); + this.update(); + }, + calculateTotal: function(data){ + this.total = 0; + helpers.each(data,function(segment){ + this.total += segment.value; + },this); + this.scale.valuesCount = this.segments.length; + }, + updateScaleRange: function(datapoints){ + var valuesArray = []; + helpers.each(datapoints,function(segment){ + valuesArray.push(segment.value); + }); + + var scaleSizes = (this.options.scaleOverride) ? + { + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + } : + helpers.calculateScaleRange( + valuesArray, + helpers.min([this.chart.width, this.chart.height])/2, + this.options.scaleFontSize, + this.options.scaleBeginAtZero, + this.options.scaleIntegersOnly + ); + + helpers.extend( + this.scale, + scaleSizes, + { + size: helpers.min([this.chart.width, this.chart.height]), + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + } + ); + + }, + update : function(){ + this.calculateTotal(this.segments); + + helpers.each(this.segments,function(segment){ + segment.save(); + }); + + this.reflow(); + this.render(); + }, + reflow : function(){ + helpers.extend(this.SegmentArc.prototype,{ + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.updateScaleRange(this.segments); + this.scale.update(); + + helpers.extend(this.scale,{ + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + }); + + helpers.each(this.segments, function(segment){ + segment.update({ + outerRadius : this.scale.calculateCenterOffset(segment.value) + }); + }, this); + + }, + draw : function(ease){ + var easingDecimal = ease || 1; + //Clear & draw the canvas + this.clear(); + helpers.each(this.segments,function(segment, index){ + segment.transition({ + circumference : this.scale.getCircumference(), + outerRadius : this.scale.calculateCenterOffset(segment.value) + },easingDecimal); + + segment.endAngle = segment.startAngle + segment.circumference; + + // If we've removed the first segment we need to set the first one to + // start at the top. + if (index === 0){ + segment.startAngle = Math.PI * 1.5; + } + + //Check to see if it's the last segment, if not get the next and update the start angle + if (index < this.segments.length - 1){ + this.segments[index+1].startAngle = segment.endAngle; + } + segment.draw(); + }, this); + this.scale.draw(); + } + }); + +}).call(this); +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + + + Chart.Type.extend({ + name: "Radar", + defaults:{ + //Boolean - Whether to show lines for each scale point + scaleShowLine : true, + + //Boolean - Whether we show the angle lines out of the radar + angleShowLineOut : true, + + //Boolean - Whether to show labels on the scale + scaleShowLabels : false, + + // Boolean - Whether the scale should begin at zero + scaleBeginAtZero : true, + + //String - Colour of the angle line + angleLineColor : "rgba(0,0,0,.1)", + + //Number - Pixel width of the angle line + angleLineWidth : 1, + + //String - Point label font declaration + pointLabelFontFamily : "'Arial'", + + //String - Point label font weight + pointLabelFontStyle : "normal", + + //Number - Point label font size in pixels + pointLabelFontSize : 10, + + //String - Point label font colour + pointLabelFontColor : "#666", + + //Boolean - Whether to show a dot for each point + pointDot : true, + + //Number - Radius of each point dot in pixels + pointDotRadius : 3, + + //Number - Pixel width of point dot stroke + pointDotStrokeWidth : 1, + + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHitDetectionRadius : 20, + + //Boolean - Whether to show a stroke for datasets + datasetStroke : true, + + //Number - Pixel width of dataset stroke + datasetStrokeWidth : 2, + + //Boolean - Whether to fill the dataset with a colour + datasetFill : true, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" + + }, + + initialize: function(data){ + this.PointClass = Chart.Point.extend({ + strokeWidth : this.options.pointDotStrokeWidth, + radius : this.options.pointDotRadius, + display: this.options.pointDot, + hitDetectionRadius : this.options.pointHitDetectionRadius, + ctx : this.chart.ctx + }); + + this.datasets = []; + + this.buildScale(data); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; + + this.eachPoints(function(point){ + point.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activePointsCollection, function(activePoint){ + activePoint.fillColor = activePoint.highlightFill; + activePoint.strokeColor = activePoint.highlightStroke; + }); + + this.showTooltip(activePointsCollection); + }); + } + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset){ + + var datasetObject = { + label: dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + pointColor : dataset.pointColor, + pointStrokeColor : dataset.pointStrokeColor, + points : [] + }; + + this.datasets.push(datasetObject); + + helpers.each(dataset.data,function(dataPoint,index){ + //Add a new point for each piece of data, passing any required data to draw. + var pointPosition; + if (!this.scale.animation){ + pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint)); + } + datasetObject.points.push(new this.PointClass({ + value : dataPoint, + label : data.labels[index], + datasetLabel: dataset.label, + x: (this.options.animation) ? this.scale.xCenter : pointPosition.x, + y: (this.options.animation) ? this.scale.yCenter : pointPosition.y, + strokeColor : dataset.pointStrokeColor, + fillColor : dataset.pointColor, + highlightFill : dataset.pointHighlightFill || dataset.pointColor, + highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor + })); + },this); + + },this); + + this.render(); + }, + eachPoints : function(callback){ + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,callback,this); + },this); + }, + + getPointsAtEvent : function(evt){ + var mousePosition = helpers.getRelativePosition(evt), + fromCenter = helpers.getAngleFromPoint({ + x: this.scale.xCenter, + y: this.scale.yCenter + }, mousePosition); + + var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount, + pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex), + activePointsCollection = []; + + // If we're at the top, make the pointIndex 0 to get the first of the array. + if (pointIndex >= this.scale.valuesCount || pointIndex < 0){ + pointIndex = 0; + } + + if (fromCenter.distance <= this.scale.drawingArea){ + helpers.each(this.datasets, function(dataset){ + activePointsCollection.push(dataset.points[pointIndex]); + }); + } + + return activePointsCollection; + }, + + buildScale : function(data){ + this.scale = new Chart.RadialScale({ + display: this.options.showScale, + fontStyle: this.options.scaleFontStyle, + fontSize: this.options.scaleFontSize, + fontFamily: this.options.scaleFontFamily, + fontColor: this.options.scaleFontColor, + showLabels: this.options.scaleShowLabels, + showLabelBackdrop: this.options.scaleShowLabelBackdrop, + backdropColor: this.options.scaleBackdropColor, + backdropPaddingY : this.options.scaleBackdropPaddingY, + backdropPaddingX: this.options.scaleBackdropPaddingX, + lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, + lineColor: this.options.scaleLineColor, + angleLineColor : this.options.angleLineColor, + angleLineWidth : (this.options.angleShowLineOut) ? this.options.angleLineWidth : 0, + // Point labels at the edge of each line + pointLabelFontColor : this.options.pointLabelFontColor, + pointLabelFontSize : this.options.pointLabelFontSize, + pointLabelFontFamily : this.options.pointLabelFontFamily, + pointLabelFontStyle : this.options.pointLabelFontStyle, + height : this.chart.height, + width: this.chart.width, + xCenter: this.chart.width/2, + yCenter: this.chart.height/2, + ctx : this.chart.ctx, + templateString: this.options.scaleLabel, + labels: data.labels, + valuesCount: data.datasets[0].data.length + }); + + this.scale.setScaleSize(); + this.updateScaleRange(data.datasets); + this.scale.buildYLabels(); + }, + updateScaleRange: function(datasets){ + var valuesArray = (function(){ + var totalDataArray = []; + helpers.each(datasets,function(dataset){ + if (dataset.data){ + totalDataArray = totalDataArray.concat(dataset.data); + } + else { + helpers.each(dataset.points, function(point){ + totalDataArray.push(point.value); + }); + } + }); + return totalDataArray; + })(); + + + var scaleSizes = (this.options.scaleOverride) ? + { + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + } : + helpers.calculateScaleRange( + valuesArray, + helpers.min([this.chart.width, this.chart.height])/2, + this.options.scaleFontSize, + this.options.scaleBeginAtZero, + this.options.scaleIntegersOnly + ); + + helpers.extend( + this.scale, + scaleSizes + ); + + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + this.scale.valuesCount++; + helpers.each(valuesArray,function(value,datasetIndex){ + var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value)); + this.datasets[datasetIndex].points.push(new this.PointClass({ + value : value, + label : label, + x: pointPosition.x, + y: pointPosition.y, + strokeColor : this.datasets[datasetIndex].pointStrokeColor, + fillColor : this.datasets[datasetIndex].pointColor + })); + },this); + + this.scale.labels.push(label); + + this.reflow(); + + this.update(); + }, + removeData : function(){ + this.scale.valuesCount--; + this.scale.labels.shift(); + helpers.each(this.datasets,function(dataset){ + dataset.points.shift(); + },this); + this.reflow(); + this.update(); + }, + update : function(){ + this.eachPoints(function(point){ + point.save(); + }); + this.reflow(); + this.render(); + }, + reflow: function(){ + helpers.extend(this.scale, { + width : this.chart.width, + height: this.chart.height, + size : helpers.min([this.chart.width, this.chart.height]), + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + }); + this.updateScaleRange(this.datasets); + this.scale.setScaleSize(); + this.scale.buildYLabels(); + }, + draw : function(ease){ + var easeDecimal = ease || 1, + ctx = this.chart.ctx; + this.clear(); + this.scale.draw(); + + helpers.each(this.datasets,function(dataset){ + + //Transition each point first so that the line and point drawing isn't out of sync + helpers.each(dataset.points,function(point,index){ + if (point.hasValue()){ + point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal); + } + },this); + + + + //Draw the line between all the points + ctx.lineWidth = this.options.datasetStrokeWidth; + ctx.strokeStyle = dataset.strokeColor; + ctx.beginPath(); + helpers.each(dataset.points,function(point,index){ + if (index === 0){ + ctx.moveTo(point.x,point.y); + } + else{ + ctx.lineTo(point.x,point.y); + } + },this); + ctx.closePath(); + ctx.stroke(); + + ctx.fillStyle = dataset.fillColor; + ctx.fill(); + + //Now draw the points over the line + //A little inefficient double looping, but better than the line + //lagging behind the point positions + helpers.each(dataset.points,function(point){ + if (point.hasValue()){ + point.draw(); + } + }); + + },this); + + } + + }); + + + + + +}).call(this); \ No newline at end of file diff --git a/vendor/assets/javascripts/chart-lib.min.js b/vendor/assets/javascripts/chart-lib.min.js deleted file mode 100644 index 3a0a2c87345..00000000000 --- a/vendor/assets/javascripts/chart-lib.min.js +++ /dev/null @@ -1,11 +0,0 @@ -/*! - * Chart.js - * http://chartjs.org/ - * Version: 1.0.2 - * - * Copyright 2015 Nick Downie - * Released under the MIT license - * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md - */ -(function(){"use strict";var t=this,i=t.Chart,e=function(t){this.canvas=t.canvas,this.ctx=t;var i=function(t,i){return t["offset"+i]?t["offset"+i]:document.defaultView.getComputedStyle(t).getPropertyValue(i)},e=this.width=i(t.canvas,"Width"),n=this.height=i(t.canvas,"Height");t.canvas.width=e,t.canvas.height=n;var e=this.width=t.canvas.width,n=this.height=t.canvas.height;return this.aspectRatio=this.width/this.height,s.retinaScale(this),this};e.defaults={global:{animation:!0,animationSteps:60,animationEasing:"easeOutQuart",showScale:!0,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleIntegersOnly:!0,scaleBeginAtZero:!1,scaleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",responsive:!1,maintainAspectRatio:!0,showTooltips:!0,customTooltips:!1,tooltipEvents:["mousemove","touchstart","touchmove","mouseout"],tooltipFillColor:"rgba(0,0,0,0.8)",tooltipFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipFontSize:14,tooltipFontStyle:"normal",tooltipFontColor:"#fff",tooltipTitleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipTitleFontSize:14,tooltipTitleFontStyle:"bold",tooltipTitleFontColor:"#fff",tooltipYPadding:6,tooltipXPadding:6,tooltipCaretSize:8,tooltipCornerRadius:6,tooltipXOffset:10,tooltipTemplate:"<%if (label){%><%=label%>: <%}%><%= value %>",multiTooltipTemplate:"<%= value %>",multiTooltipKeyBackground:"#fff",onAnimationProgress:function(){},onAnimationComplete:function(){}}},e.types={};var s=e.helpers={},n=s.each=function(t,i,e){var s=Array.prototype.slice.call(arguments,3);if(t)if(t.length===+t.length){var n;for(n=0;n=0;s--){var n=t[s];if(i(n))return n}},s.inherits=function(t){var i=this,e=t&&t.hasOwnProperty("constructor")?t.constructor:function(){return i.apply(this,arguments)},s=function(){this.constructor=e};return s.prototype=i.prototype,e.prototype=new s,e.extend=r,t&&a(e.prototype,t),e.__super__=i.prototype,e}),c=s.noop=function(){},u=s.uid=function(){var t=0;return function(){return"chart-"+t++}}(),d=s.warn=function(t){window.console&&"function"==typeof window.console.warn&&console.warn(t)},p=s.amd="function"==typeof define&&define.amd,f=s.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},g=s.max=function(t){return Math.max.apply(Math,t)},m=s.min=function(t){return Math.min.apply(Math,t)},v=(s.cap=function(t,i,e){if(f(i)){if(t>i)return i}else if(f(e)&&e>t)return e;return t},s.getDecimalPlaces=function(t){return t%1!==0&&f(t)?t.toString().split(".")[1].length:0}),S=s.radians=function(t){return t*(Math.PI/180)},x=(s.getAngleFromPoint=function(t,i){var e=i.x-t.x,s=i.y-t.y,n=Math.sqrt(e*e+s*s),o=2*Math.PI+Math.atan2(s,e);return 0>e&&0>s&&(o+=2*Math.PI),{angle:o,distance:n}},s.aliasPixel=function(t){return t%2===0?0:.5}),y=(s.splineCurve=function(t,i,e,s){var n=Math.sqrt(Math.pow(i.x-t.x,2)+Math.pow(i.y-t.y,2)),o=Math.sqrt(Math.pow(e.x-i.x,2)+Math.pow(e.y-i.y,2)),a=s*n/(n+o),h=s*o/(n+o);return{inner:{x:i.x-a*(e.x-t.x),y:i.y-a*(e.y-t.y)},outer:{x:i.x+h*(e.x-t.x),y:i.y+h*(e.y-t.y)}}},s.calculateOrderOfMagnitude=function(t){return Math.floor(Math.log(t)/Math.LN10)}),C=(s.calculateScaleRange=function(t,i,e,s,n){var o=2,a=Math.floor(i/(1.5*e)),h=o>=a,l=g(t),r=m(t);l===r&&(l+=.5,r>=.5&&!s?r-=.5:l+=.5);for(var c=Math.abs(l-r),u=y(c),d=Math.ceil(l/(1*Math.pow(10,u)))*Math.pow(10,u),p=s?0:Math.floor(r/(1*Math.pow(10,u)))*Math.pow(10,u),f=d-p,v=Math.pow(10,u),S=Math.round(f/v);(S>a||a>2*S)&&!h;)if(S>a)v*=2,S=Math.round(f/v),S%1!==0&&(h=!0);else if(n&&u>=0){if(v/2%1!==0)break;v/=2,S=Math.round(f/v)}else v/=2,S=Math.round(f/v);return h&&(S=o,v=f/S),{steps:S,stepValue:v,min:p,max:p+S*v}},s.template=function(t,i){function e(t,i){var e=/\W/.test(t)?new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+t.replace(/[\r\t\n]/g," ").split("<%").join(" ").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split(" ").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');"):s[t]=s[t];return i?e(i):e}if(t instanceof Function)return t(i);var s={};return e(t,i)}),w=(s.generateLabels=function(t,i,e,s){var o=new Array(i);return labelTemplateString&&n(o,function(i,n){o[n]=C(t,{value:e+s*(n+1)})}),o},s.easingEffects={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-1*t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-0.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return 1*((t=t/1-1)*t*t+1)},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-1*((t=t/1-1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-0.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return 1*(t/=1)*t*t*t*t},easeOutQuint:function(t){return 1*((t=t/1-1)*t*t*t*t+1)},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return-1*Math.cos(t/1*(Math.PI/2))+1},easeOutSine:function(t){return 1*Math.sin(t/1*(Math.PI/2))},easeInOutSine:function(t){return-0.5*(Math.cos(Math.PI*t/1)-1)},easeInExpo:function(t){return 0===t?1:1*Math.pow(2,10*(t/1-1))},easeOutExpo:function(t){return 1===t?1:1*(-Math.pow(2,-10*t/1)+1)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(-Math.pow(2,-10*--t)+2)},easeInCirc:function(t){return t>=1?t:-1*(Math.sqrt(1-(t/=1)*t)-1)},easeOutCirc:function(t){return 1*Math.sqrt(1-(t=t/1-1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-0.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),st?-.5*s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e):s*Math.pow(2,-10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)*.5+1)},easeInBack:function(t){var i=1.70158;return 1*(t/=1)*t*((i+1)*t-i)},easeOutBack:function(t){var i=1.70158;return 1*((t=t/1-1)*t*((i+1)*t+i)+1)},easeInOutBack:function(t){var i=1.70158;return(t/=.5)<1?.5*t*t*(((i*=1.525)+1)*t-i):.5*((t-=2)*t*(((i*=1.525)+1)*t+i)+2)},easeInBounce:function(t){return 1-w.easeOutBounce(1-t)},easeOutBounce:function(t){return(t/=1)<1/2.75?7.5625*t*t:2/2.75>t?1*(7.5625*(t-=1.5/2.75)*t+.75):2.5/2.75>t?1*(7.5625*(t-=2.25/2.75)*t+.9375):1*(7.5625*(t-=2.625/2.75)*t+.984375)},easeInOutBounce:function(t){return.5>t?.5*w.easeInBounce(2*t):.5*w.easeOutBounce(2*t-1)+.5}}),b=s.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)}}(),P=s.cancelAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.oCancelAnimationFrame||window.msCancelAnimationFrame||function(t){return window.clearTimeout(t,1e3/60)}}(),L=(s.animationLoop=function(t,i,e,s,n,o){var a=0,h=w[e]||w.linear,l=function(){a++;var e=a/i,r=h(e);t.call(o,r,e,a),s.call(o,r,e),i>a?o.animationFrame=b(l):n.apply(o)};b(l)},s.getRelativePosition=function(t){var i,e,s=t.originalEvent||t,n=t.currentTarget||t.srcElement,o=n.getBoundingClientRect();return s.touches?(i=s.touches[0].clientX-o.left,e=s.touches[0].clientY-o.top):(i=s.clientX-o.left,e=s.clientY-o.top),{x:i,y:e}},s.addEvent=function(t,i,e){t.addEventListener?t.addEventListener(i,e):t.attachEvent?t.attachEvent("on"+i,e):t["on"+i]=e}),k=s.removeEvent=function(t,i,e){t.removeEventListener?t.removeEventListener(i,e,!1):t.detachEvent?t.detachEvent("on"+i,e):t["on"+i]=c},F=(s.bindEvents=function(t,i,e){t.events||(t.events={}),n(i,function(i){t.events[i]=function(){e.apply(t,arguments)},L(t.chart.canvas,i,t.events[i])})},s.unbindEvents=function(t,i){n(i,function(i,e){k(t.chart.canvas,e,i)})}),R=s.getMaximumWidth=function(t){var i=t.parentNode;return i.clientWidth},T=s.getMaximumHeight=function(t){var i=t.parentNode;return i.clientHeight},A=(s.getMaximumSize=s.getMaximumWidth,s.retinaScale=function(t){var i=t.ctx,e=t.canvas.width,s=t.canvas.height;window.devicePixelRatio&&(i.canvas.style.width=e+"px",i.canvas.style.height=s+"px",i.canvas.height=s*window.devicePixelRatio,i.canvas.width=e*window.devicePixelRatio,i.scale(window.devicePixelRatio,window.devicePixelRatio))}),M=s.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},W=s.fontString=function(t,i,e){return i+" "+t+"px "+e},z=s.longestText=function(t,i,e){t.font=i;var s=0;return n(e,function(i){var e=t.measureText(i).width;s=e>s?e:s}),s},B=s.drawRoundedRectangle=function(t,i,e,s,n,o){t.beginPath(),t.moveTo(i+o,e),t.lineTo(i+s-o,e),t.quadraticCurveTo(i+s,e,i+s,e+o),t.lineTo(i+s,e+n-o),t.quadraticCurveTo(i+s,e+n,i+s-o,e+n),t.lineTo(i+o,e+n),t.quadraticCurveTo(i,e+n,i,e+n-o),t.lineTo(i,e+o),t.quadraticCurveTo(i,e,i+o,e),t.closePath()};e.instances={},e.Type=function(t,i,s){this.options=i,this.chart=s,this.id=u(),e.instances[this.id]=this,i.responsive&&this.resize(),this.initialize.call(this,t)},a(e.Type.prototype,{initialize:function(){return this},clear:function(){return M(this.chart),this},stop:function(){return P(this.animationFrame),this},resize:function(t){this.stop();var i=this.chart.canvas,e=R(this.chart.canvas),s=this.options.maintainAspectRatio?e/this.chart.aspectRatio:T(this.chart.canvas);return i.width=this.chart.width=e,i.height=this.chart.height=s,A(this.chart),"function"==typeof t&&t.apply(this,Array.prototype.slice.call(arguments,1)),this},reflow:c,render:function(t){return t&&this.reflow(),this.options.animation&&!t?s.animationLoop(this.draw,this.options.animationSteps,this.options.animationEasing,this.options.onAnimationProgress,this.options.onAnimationComplete,this):(this.draw(),this.options.onAnimationComplete.call(this)),this},generateLegend:function(){return C(this.options.legendTemplate,this)},destroy:function(){this.clear(),F(this,this.events);var t=this.chart.canvas;t.width=this.chart.width,t.height=this.chart.height,t.style.removeProperty?(t.style.removeProperty("width"),t.style.removeProperty("height")):(t.style.removeAttribute("width"),t.style.removeAttribute("height")),delete e.instances[this.id]},showTooltip:function(t,i){"undefined"==typeof this.activeElements&&(this.activeElements=[]);var o=function(t){var i=!1;return t.length!==this.activeElements.length?i=!0:(n(t,function(t,e){t!==this.activeElements[e]&&(i=!0)},this),i)}.call(this,t);if(o||i){if(this.activeElements=t,this.draw(),this.options.customTooltips&&this.options.customTooltips(!1),t.length>0)if(this.datasets&&this.datasets.length>1){for(var a,h,r=this.datasets.length-1;r>=0&&(a=this.datasets[r].points||this.datasets[r].bars||this.datasets[r].segments,h=l(a,t[0]),-1===h);r--);var c=[],u=[],d=function(){var t,i,e,n,o,a=[],l=[],r=[];return s.each(this.datasets,function(i){t=i.points||i.bars||i.segments,t[h]&&t[h].hasValue()&&a.push(t[h])}),s.each(a,function(t){l.push(t.x),r.push(t.y),c.push(s.template(this.options.multiTooltipTemplate,t)),u.push({fill:t._saved.fillColor||t.fillColor,stroke:t._saved.strokeColor||t.strokeColor})},this),o=m(r),e=g(r),n=m(l),i=g(l),{x:n>this.chart.width/2?n:i,y:(o+e)/2}}.call(this,h);new e.MultiTooltip({x:d.x,y:d.y,xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,xOffset:this.options.tooltipXOffset,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,titleTextColor:this.options.tooltipTitleFontColor,titleFontFamily:this.options.tooltipTitleFontFamily,titleFontStyle:this.options.tooltipTitleFontStyle,titleFontSize:this.options.tooltipTitleFontSize,cornerRadius:this.options.tooltipCornerRadius,labels:c,legendColors:u,legendColorBackground:this.options.multiTooltipKeyBackground,title:t[0].label,chart:this.chart,ctx:this.chart.ctx,custom:this.options.customTooltips}).draw()}else n(t,function(t){var i=t.tooltipPosition();new e.Tooltip({x:Math.round(i.x),y:Math.round(i.y),xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,caretHeight:this.options.tooltipCaretSize,cornerRadius:this.options.tooltipCornerRadius,text:C(this.options.tooltipTemplate,t),chart:this.chart,custom:this.options.customTooltips}).draw()},this);return this}},toBase64Image:function(){return this.chart.canvas.toDataURL.apply(this.chart.canvas,arguments)}}),e.Type.extend=function(t){var i=this,s=function(){return i.apply(this,arguments)};if(s.prototype=o(i.prototype),a(s.prototype,t),s.extend=e.Type.extend,t.name||i.prototype.name){var n=t.name||i.prototype.name,l=e.defaults[i.prototype.name]?o(e.defaults[i.prototype.name]):{};e.defaults[n]=a(l,t.defaults),e.types[n]=s,e.prototype[n]=function(t,i){var o=h(e.defaults.global,e.defaults[n],i||{});return new s(t,o,this)}}else d("Name not provided for this chart, so it hasn't been registered");return i},e.Element=function(t){a(this,t),this.initialize.apply(this,arguments),this.save()},a(e.Element.prototype,{initialize:function(){},restore:function(t){return t?n(t,function(t){this[t]=this._saved[t]},this):a(this,this._saved),this},save:function(){return this._saved=o(this),delete this._saved._saved,this},update:function(t){return n(t,function(t,i){this._saved[i]=this[i],this[i]=t},this),this},transition:function(t,i){return n(t,function(t,e){this[e]=(t-this._saved[e])*i+this._saved[e]},this),this},tooltipPosition:function(){return{x:this.x,y:this.y}},hasValue:function(){return f(this.value)}}),e.Element.extend=r,e.Point=e.Element.extend({display:!0,inRange:function(t,i){var e=this.hitDetectionRadius+this.radius;return Math.pow(t-this.x,2)+Math.pow(i-this.y,2)=this.startAngle&&e.angle<=this.endAngle,o=e.distance>=this.innerRadius&&e.distance<=this.outerRadius;return n&&o},tooltipPosition:function(){var t=this.startAngle+(this.endAngle-this.startAngle)/2,i=(this.outerRadius-this.innerRadius)/2+this.innerRadius;return{x:this.x+Math.cos(t)*i,y:this.y+Math.sin(t)*i}},draw:function(t){var i=this.ctx;i.beginPath(),i.arc(this.x,this.y,this.outerRadius,this.startAngle,this.endAngle),i.arc(this.x,this.y,this.innerRadius,this.endAngle,this.startAngle,!0),i.closePath(),i.strokeStyle=this.strokeColor,i.lineWidth=this.strokeWidth,i.fillStyle=this.fillColor,i.fill(),i.lineJoin="bevel",this.showStroke&&i.stroke()}}),e.Rectangle=e.Element.extend({draw:function(){var t=this.ctx,i=this.width/2,e=this.x-i,s=this.x+i,n=this.base-(this.base-this.y),o=this.strokeWidth/2;this.showStroke&&(e+=o,s-=o,n+=o),t.beginPath(),t.fillStyle=this.fillColor,t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.moveTo(e,this.base),t.lineTo(e,n),t.lineTo(s,n),t.lineTo(s,this.base),t.fill(),this.showStroke&&t.stroke()},height:function(){return this.base-this.y},inRange:function(t,i){return t>=this.x-this.width/2&&t<=this.x+this.width/2&&i>=this.y&&i<=this.base}}),e.Tooltip=e.Element.extend({draw:function(){var t=this.chart.ctx;t.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.xAlign="center",this.yAlign="above";var i=this.caretPadding=2,e=t.measureText(this.text).width+2*this.xPadding,s=this.fontSize+2*this.yPadding,n=s+this.caretHeight+i;this.x+e/2>this.chart.width?this.xAlign="left":this.x-e/2<0&&(this.xAlign="right"),this.y-n<0&&(this.yAlign="below");var o=this.x-e/2,a=this.y-n;if(t.fillStyle=this.fillColor,this.custom)this.custom(this);else{switch(this.yAlign){case"above":t.beginPath(),t.moveTo(this.x,this.y-i),t.lineTo(this.x+this.caretHeight,this.y-(i+this.caretHeight)),t.lineTo(this.x-this.caretHeight,this.y-(i+this.caretHeight)),t.closePath(),t.fill();break;case"below":a=this.y+i+this.caretHeight,t.beginPath(),t.moveTo(this.x,this.y+i),t.lineTo(this.x+this.caretHeight,this.y+i+this.caretHeight),t.lineTo(this.x-this.caretHeight,this.y+i+this.caretHeight),t.closePath(),t.fill()}switch(this.xAlign){case"left":o=this.x-e+(this.cornerRadius+this.caretHeight);break;case"right":o=this.x-(this.cornerRadius+this.caretHeight)}B(t,o,a,e,s,this.cornerRadius),t.fill(),t.fillStyle=this.textColor,t.textAlign="center",t.textBaseline="middle",t.fillText(this.text,o+e/2,a+s/2)}}}),e.MultiTooltip=e.Element.extend({initialize:function(){this.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.titleFont=W(this.titleFontSize,this.titleFontStyle,this.titleFontFamily),this.height=this.labels.length*this.fontSize+(this.labels.length-1)*(this.fontSize/2)+2*this.yPadding+1.5*this.titleFontSize,this.ctx.font=this.titleFont;var t=this.ctx.measureText(this.title).width,i=z(this.ctx,this.font,this.labels)+this.fontSize+3,e=g([i,t]);this.width=e+2*this.xPadding;var s=this.height/2;this.y-s<0?this.y=s:this.y+s>this.chart.height&&(this.y=this.chart.height-s),this.x>this.chart.width/2?this.x-=this.xOffset+this.width:this.x+=this.xOffset},getLineHeight:function(t){var i=this.y-this.height/2+this.yPadding,e=t-1;return 0===t?i+this.titleFontSize/2:i+(1.5*this.fontSize*e+this.fontSize/2)+1.5*this.titleFontSize},draw:function(){if(this.custom)this.custom(this);else{B(this.ctx,this.x,this.y-this.height/2,this.width,this.height,this.cornerRadius);var t=this.ctx;t.fillStyle=this.fillColor,t.fill(),t.closePath(),t.textAlign="left",t.textBaseline="middle",t.fillStyle=this.titleTextColor,t.font=this.titleFont,t.fillText(this.title,this.x+this.xPadding,this.getLineHeight(0)),t.font=this.font,s.each(this.labels,function(i,e){t.fillStyle=this.textColor,t.fillText(i,this.x+this.xPadding+this.fontSize+3,this.getLineHeight(e+1)),t.fillStyle=this.legendColorBackground,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize),t.fillStyle=this.legendColors[e].fill,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize)},this)}}}),e.Scale=e.Element.extend({initialize:function(){this.fit()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}));this.yLabelWidth=this.display&&this.showLabels?z(this.ctx,this.font,this.yLabels):0},addXLabel:function(t){this.xLabels.push(t),this.valuesCount++,this.fit()},removeXLabel:function(){this.xLabels.shift(),this.valuesCount--,this.fit()},fit:function(){this.startPoint=this.display?this.fontSize:0,this.endPoint=this.display?this.height-1.5*this.fontSize-5:this.height,this.startPoint+=this.padding,this.endPoint-=this.padding;var t,i=this.endPoint-this.startPoint;for(this.calculateYRange(i),this.buildYLabels(),this.calculateXLabelRotation();i>this.endPoint-this.startPoint;)i=this.endPoint-this.startPoint,t=this.yLabelWidth,this.calculateYRange(i),this.buildYLabels(),tthis.yLabelWidth+10?e/2:this.yLabelWidth+10,this.xLabelRotation=0,this.display){var n,o=z(this.ctx,this.font,this.xLabels);this.xLabelWidth=o;for(var a=Math.floor(this.calculateX(1)-this.calculateX(0))-6;this.xLabelWidth>a&&0===this.xLabelRotation||this.xLabelWidth>a&&this.xLabelRotation<=90&&this.xLabelRotation>0;)n=Math.cos(S(this.xLabelRotation)),t=n*e,i=n*s,t+this.fontSize/2>this.yLabelWidth+8&&(this.xScalePaddingLeft=t+this.fontSize/2),this.xScalePaddingRight=this.fontSize/2,this.xLabelRotation++,this.xLabelWidth=n*o;this.xLabelRotation>0&&(this.endPoint-=Math.sin(S(this.xLabelRotation))*o+3)}else this.xLabelWidth=0,this.xScalePaddingRight=this.padding,this.xScalePaddingLeft=this.padding},calculateYRange:c,drawingArea:function(){return this.startPoint-this.endPoint},calculateY:function(t){var i=this.drawingArea()/(this.min-this.max);return this.endPoint-i*(t-this.min)},calculateX:function(t){var i=(this.xLabelRotation>0,this.width-(this.xScalePaddingLeft+this.xScalePaddingRight)),e=i/Math.max(this.valuesCount-(this.offsetGridLines?0:1),1),s=e*t+this.xScalePaddingLeft;return this.offsetGridLines&&(s+=e/2),Math.round(s)},update:function(t){s.extend(this,t),this.fit()},draw:function(){var t=this.ctx,i=(this.endPoint-this.startPoint)/this.steps,e=Math.round(this.xScalePaddingLeft);this.display&&(t.fillStyle=this.textColor,t.font=this.font,n(this.yLabels,function(n,o){var a=this.endPoint-i*o,h=Math.round(a),l=this.showHorizontalLines;t.textAlign="right",t.textBaseline="middle",this.showLabels&&t.fillText(n,e-10,a),0!==o||l||(l=!0),l&&t.beginPath(),o>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),h+=s.aliasPixel(t.lineWidth),l&&(t.moveTo(e,h),t.lineTo(this.width,h),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(e-5,h),t.lineTo(e,h),t.stroke(),t.closePath()},this),n(this.xLabels,function(i,e){var s=this.calculateX(e)+x(this.lineWidth),n=this.calculateX(e-(this.offsetGridLines?.5:0))+x(this.lineWidth),o=this.xLabelRotation>0,a=this.showVerticalLines;0!==e||a||(a=!0),a&&t.beginPath(),e>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),a&&(t.moveTo(n,this.endPoint),t.lineTo(n,this.startPoint-3),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(n,this.endPoint),t.lineTo(n,this.endPoint+5),t.stroke(),t.closePath(),t.save(),t.translate(s,o?this.endPoint+12:this.endPoint+8),t.rotate(-1*S(this.xLabelRotation)),t.font=this.font,t.textAlign=o?"right":"center",t.textBaseline=o?"middle":"top",t.fillText(i,0,0),t.restore()},this))}}),e.RadialScale=e.Element.extend({initialize:function(){this.size=m([this.height,this.width]),this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2},calculateCenterOffset:function(t){var i=this.drawingArea/(this.max-this.min);return(t-this.min)*i},update:function(){this.lineArc?this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2:this.setScaleSize(),this.buildYLabels()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}))},getCircumference:function(){return 2*Math.PI/this.valuesCount},setScaleSize:function(){var t,i,e,s,n,o,a,h,l,r,c,u,d=m([this.height/2-this.pointLabelFontSize-5,this.width/2]),p=this.width,g=0;for(this.ctx.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),i=0;ip&&(p=t.x+s,n=i),t.x-sp&&(p=t.x+e,n=i):i>this.valuesCount/2&&t.x-e0){var s,n=e*(this.drawingArea/this.steps),o=this.yCenter-n;if(this.lineWidth>0)if(t.strokeStyle=this.lineColor,t.lineWidth=this.lineWidth,this.lineArc)t.beginPath(),t.arc(this.xCenter,this.yCenter,n,0,2*Math.PI),t.closePath(),t.stroke();else{t.beginPath();for(var a=0;a=0;i--){if(this.angleLineWidth>0){var e=this.getPointPosition(i,this.calculateCenterOffset(this.max));t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(e.x,e.y),t.stroke(),t.closePath()}var s=this.getPointPosition(i,this.calculateCenterOffset(this.max)+5);t.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),t.fillStyle=this.pointLabelFontColor;var o=this.labels.length,a=this.labels.length/2,h=a/2,l=h>i||i>o-h,r=i===h||i===o-h;t.textAlign=0===i?"center":i===a?"center":a>i?"left":"right",t.textBaseline=r?"middle":l?"bottom":"top",t.fillText(this.labels[i],s.x,s.y)}}}}}),s.addEvent(window,"resize",function(){var t;return function(){clearTimeout(t),t=setTimeout(function(){n(e.instances,function(t){t.options.responsive&&t.resize(t.render,!0)})},50)}}()),p?define(function(){return e}):"object"==typeof module&&module.exports&&(module.exports=e),t.Chart=e,e.noConflict=function(){return t.Chart=i,e}}).call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleBeginAtZero:!0,scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,scaleShowHorizontalLines:!0,scaleShowVerticalLines:!0,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,legendTemplate:'
    <% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
'};i.Type.extend({name:"Bar",defaults:s,initialize:function(t){var s=this.options;this.ScaleClass=i.Scale.extend({offsetGridLines:!0,calculateBarX:function(t,i,e){var n=this.calculateBaseWidth(),o=this.calculateX(e)-n/2,a=this.calculateBarWidth(t);return o+a*i+i*s.barDatasetSpacing+a/2},calculateBaseWidth:function(){return this.calculateX(1)-this.calculateX(0)-2*s.barValueSpacing},calculateBarWidth:function(t){var i=this.calculateBaseWidth()-(t-1)*s.barDatasetSpacing;return i/t}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getBarsAtEvent(t):[];this.eachBars(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),this.BarClass=i.Rectangle.extend({strokeWidth:this.options.barStrokeWidth,showStroke:this.options.barShowStroke,ctx:this.chart.ctx}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,bars:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.bars.push(new this.BarClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.strokeColor,fillColor:i.fillColor,highlightFill:i.highlightFill||i.fillColor,highlightStroke:i.highlightStroke||i.strokeColor}))},this)},this),this.buildScale(t.labels),this.BarClass.prototype.base=this.scale.endPoint,this.eachBars(function(t,i,s){e.extend(t,{width:this.scale.calculateBarWidth(this.datasets.length),x:this.scale.calculateBarX(this.datasets.length,s,i),y:this.scale.endPoint}),t.save()},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachBars(function(t){t.save()}),this.render()},eachBars:function(t){e.each(this.datasets,function(i,s){e.each(i.bars,t,this,s)},this)},getBarsAtEvent:function(t){for(var i,s=[],n=e.getRelativePosition(t),o=function(t){s.push(t.bars[i])},a=0;a<% for (var i=0; i
  • <%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>'};i.Type.extend({name:"Doughnut",defaults:s,initialize:function(t){this.segments=[],this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,this.SegmentArc=i.Arc.extend({ctx:this.chart.ctx,x:this.chart.width/2,y:this.chart.height/2}),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.calculateTotal(t),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({value:t.value,outerRadius:this.options.animateScale?0:this.outerRadius,innerRadius:this.options.animateScale?0:this.outerRadius/100*this.options.percentageInnerCutout,fillColor:t.color,highlightColor:t.highlight||t.color,showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,startAngle:1.5*Math.PI,circumference:this.options.animateRotate?0:this.calculateCircumference(t.value),label:t.label})),e||(this.reflow(),this.update())},calculateCircumference:function(t){return 2*Math.PI*(Math.abs(t)/this.total)},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=Math.abs(t.value)},this)},update:function(){this.calculateTotal(this.segments),e.each(this.activeElements,function(t){t.restore(["fillColor"])}),e.each(this.segments,function(t){t.save()}),this.render()},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,e.each(this.segments,function(t){t.update({outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout})},this)},draw:function(t){var i=t?t:1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.calculateCircumference(t.value),outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout},i),t.endAngle=t.startAngle+t.circumference,t.draw(),0===e&&(t.startAngle=1.5*Math.PI),e<% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>'};i.Type.extend({name:"Line",defaults:s,initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx,inRange:function(t){return Math.pow(t-this.x,2)0&&ithis.scale.endPoint?t.controlPoints.outer.y=this.scale.endPoint:t.controlPoints.outer.ythis.scale.endPoint?t.controlPoints.inner.y=this.scale.endPoint:t.controlPoints.inner.y0&&(s.lineTo(h[h.length-1].x,this.scale.endPoint),s.lineTo(h[0].x,this.scale.endPoint),s.fillStyle=t.fillColor,s.closePath(),s.fill()),e.each(h,function(t){t.draw()})},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBeginAtZero:!0,scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,scaleShowLine:!0,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'
      <% for (var i=0; i
    • <%if(segments[i].label){%><%=segments[i].label%><%}%>
    • <%}%>
    '};i.Type.extend({name:"PolarArea",defaults:s,initialize:function(t){this.segments=[],this.SegmentArc=i.Arc.extend({showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,ctx:this.chart.ctx,innerRadius:0,x:this.chart.width/2,y:this.chart.height/2}),this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,lineArc:!0,width:this.chart.width,height:this.chart.height,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,valuesCount:t.length}),this.updateScaleRange(t),this.scale.update(),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({fillColor:t.color,highlightColor:t.highlight||t.color,label:t.label,value:t.value,outerRadius:this.options.animateScale?0:this.scale.calculateCenterOffset(t.value),circumference:this.options.animateRotate?0:this.scale.getCircumference(),startAngle:1.5*Math.PI})),e||(this.reflow(),this.update())},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this),this.scale.valuesCount=this.segments.length},updateScaleRange:function(t){var i=[];e.each(t,function(t){i.push(t.value)});var s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s,{size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2})},update:function(){this.calculateTotal(this.segments),e.each(this.segments,function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.updateScaleRange(this.segments),this.scale.update(),e.extend(this.scale,{xCenter:this.chart.width/2,yCenter:this.chart.height/2}),e.each(this.segments,function(t){t.update({outerRadius:this.scale.calculateCenterOffset(t.value)})},this)},draw:function(t){var i=t||1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.scale.getCircumference(),outerRadius:this.scale.calculateCenterOffset(t.value)},i),t.endAngle=t.startAngle+t.circumference,0===e&&(t.startAngle=1.5*Math.PI),e<% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>'},initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx}),this.datasets=[],this.buildScale(t),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){var o;this.scale.animation||(o=this.scale.getPointPosition(n,this.scale.calculateCenterOffset(e))),s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,x:this.options.animation?this.scale.xCenter:o.x,y:this.options.animation?this.scale.yCenter:o.y,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this)},this),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=e.getRelativePosition(t),s=e.getAngleFromPoint({x:this.scale.xCenter,y:this.scale.yCenter},i),n=2*Math.PI/this.scale.valuesCount,o=Math.round((s.angle-1.5*Math.PI)/n),a=[];return(o>=this.scale.valuesCount||0>o)&&(o=0),s.distance<=this.scale.drawingArea&&e.each(this.datasets,function(t){a.push(t.points[o])}),a},buildScale:function(t){this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,angleLineColor:this.options.angleLineColor,angleLineWidth:this.options.angleShowLineOut?this.options.angleLineWidth:0,pointLabelFontColor:this.options.pointLabelFontColor,pointLabelFontSize:this.options.pointLabelFontSize,pointLabelFontFamily:this.options.pointLabelFontFamily,pointLabelFontStyle:this.options.pointLabelFontStyle,height:this.chart.height,width:this.chart.width,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,labels:t.labels,valuesCount:t.datasets[0].data.length}),this.scale.setScaleSize(),this.updateScaleRange(t.datasets),this.scale.buildYLabels()},updateScaleRange:function(t){var i=function(){var i=[];return e.each(t,function(t){t.data?i=i.concat(t.data):e.each(t.points,function(t){i.push(t.value)})}),i}(),s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s)},addData:function(t,i){this.scale.valuesCount++,e.each(t,function(t,e){var s=this.scale.getPointPosition(this.scale.valuesCount,this.scale.calculateCenterOffset(t));this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:s.x,y:s.y,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.labels.push(i),this.reflow(),this.update()},removeData:function(){this.scale.valuesCount--,this.scale.labels.shift(),e.each(this.datasets,function(t){t.points.shift()},this),this.reflow(),this.update()},update:function(){this.eachPoints(function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.scale,{width:this.chart.width,height:this.chart.height,size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2}),this.updateScaleRange(this.datasets),this.scale.setScaleSize(),this.scale.buildYLabels()},draw:function(t){var i=t||1,s=this.chart.ctx;this.clear(),this.scale.draw(),e.each(this.datasets,function(t){e.each(t.points,function(t,e){t.hasValue()&&t.transition(this.scale.getPointPosition(e,this.scale.calculateCenterOffset(t.value)),i)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(t.points,function(t,i){0===i?s.moveTo(t.x,t.y):s.lineTo(t.x,t.y)},this),s.closePath(),s.stroke(),s.fillStyle=t.fillColor,s.fill(),e.each(t.points,function(t){t.hasValue()&&t.draw()})},this)}})}.call(this); \ No newline at end of file -- cgit v1.2.1 From 37d3728dc5f77bb0f3edbf21b2e1f3128b72df2a Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 19 Jan 2016 19:46:30 -0200 Subject: Don't vendor minified g.raphael.js --- app/assets/javascripts/application.js.coffee | 2 +- vendor/assets/javascripts/g.raphael-min.js | 7 - vendor/assets/javascripts/g.raphael.js | 861 +++++++++++++++++++++++++++ 3 files changed, 862 insertions(+), 8 deletions(-) delete mode 100644 vendor/assets/javascripts/g.raphael-min.js create mode 100644 vendor/assets/javascripts/g.raphael.js diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 5e5c5a916a8..1033c383550 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -21,7 +21,7 @@ #= require bootstrap #= require select2 #= require raphael -#= require g.raphael-min +#= require g.raphael #= require g.bar-min #= require Chart #= require branch-graph diff --git a/vendor/assets/javascripts/g.raphael-min.js b/vendor/assets/javascripts/g.raphael-min.js deleted file mode 100644 index f8b381c623b..00000000000 --- a/vendor/assets/javascripts/g.raphael-min.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * g.Raphael 0.51 - Charting library, based on Raphaël - * - * Copyright (c) 2009-2012 Dmitry Baranovskiy (http://g.raphaeljs.com) - * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license. - */ -Raphael.el.popup=function(a,b,c,d){var f,g,h,i,j,e=this.paper||this[0].paper;if(e){switch(this.type){case"text":case"circle":case"ellipse":h=!0;break;default:h=!1}a=null==a?"up":a,b=b||5,f=this.getBBox(),c="number"==typeof c?c:h?f.x+f.width/2:f.x,d="number"==typeof d?d:h?f.y+f.height/2:f.y,i=Math.max(f.width/2-b,0),j=Math.max(f.height/2-b,0),this.translate(c-f.x-(h?f.width/2:0),d-f.y-(h?f.height/2:0)),f=this.getBBox();var k={up:["M",c,d,"l",-b,-b,-i,0,"a",b,b,0,0,1,-b,-b,"l",0,-f.height,"a",b,b,0,0,1,b,-b,"l",2*b+2*i,0,"a",b,b,0,0,1,b,b,"l",0,f.height,"a",b,b,0,0,1,-b,b,"l",-i,0,"z"].join(","),down:["M",c,d,"l",b,b,i,0,"a",b,b,0,0,1,b,b,"l",0,f.height,"a",b,b,0,0,1,-b,b,"l",-(2*b+2*i),0,"a",b,b,0,0,1,-b,-b,"l",0,-f.height,"a",b,b,0,0,1,b,-b,"l",i,0,"z"].join(","),left:["M",c,d,"l",-b,b,0,j,"a",b,b,0,0,1,-b,b,"l",-f.width,0,"a",b,b,0,0,1,-b,-b,"l",0,-(2*b+2*j),"a",b,b,0,0,1,b,-b,"l",f.width,0,"a",b,b,0,0,1,b,b,"l",0,j,"z"].join(","),right:["M",c,d,"l",b,-b,0,-j,"a",b,b,0,0,1,b,-b,"l",f.width,0,"a",b,b,0,0,1,b,b,"l",0,2*b+2*j,"a",b,b,0,0,1,-b,b,"l",-f.width,0,"a",b,b,0,0,1,-b,-b,"l",0,-j,"z"].join(",")};return g={up:{x:-!h*(f.width/2),y:2*-b-(h?f.height/2:f.height)},down:{x:-!h*(f.width/2),y:2*b+(h?f.height/2:f.height)},left:{x:2*-b-(h?f.width/2:f.width),y:-!h*(f.height/2)},right:{x:2*b+(h?f.width/2:f.width),y:-!h*(f.height/2)}}[a],this.translate(g.x,g.y),e.path(k[a]).attr({fill:"#000",stroke:"none"}).insertBefore(this.node?this:this[0])}},Raphael.el.tag=function(a,b,c,d){var e=3,f=this.paper||this[0].paper;if(f){var i,j,k,g=f.path().attr({fill:"#000",stroke:"#000"}),h=this.getBBox();switch(this.type){case"text":case"circle":case"ellipse":k=!0;break;default:k=!1}return a=a||0,c="number"==typeof c?c:k?h.x+h.width/2:h.x,d="number"==typeof d?d:k?h.y+h.height/2:h.y,b=null==b?5:b,j=.5522*b,h.height>=2*b?g.attr({path:["M",c,d+b,"a",b,b,0,1,1,0,2*-b,b,b,0,1,1,0,2*b,"m",0,2*-b-e,"a",b+e,b+e,0,1,0,0,2*(b+e),"L",c+b+e,d+h.height/2+e,"l",h.width+2*e,0,0,-h.height-2*e,-h.width-2*e,0,"L",c,d-b-e].join(",")}):(i=Math.sqrt(Math.pow(b+e,2)-Math.pow(h.height/2+e,2)),g.attr({path:["M",c,d+b,"c",-j,0,-b,j-b,-b,-b,0,-j,b-j,-b,b,-b,j,0,b,b-j,b,b,0,j,j-b,b,-b,b,"M",c+i,d-h.height/2-e,"a",b+e,b+e,0,1,0,0,h.height+2*e,"l",b+e-i+h.width+2*e,0,0,-h.height-2*e,"L",c+i,d-h.height/2-e].join(",")})),a=360-a,g.rotate(a,c,d),this.attrs?(this.attr(this.attrs.x?"x":"cx",c+b+e+(k?h.width/2:"text"==this.type?h.width:0)).attr("y",k?d:d-h.height/2),this.rotate(a,c,d),a>90&&270>a&&this.attr(this.attrs.x?"x":"cx",c-b-e-(k?h.width/2:h.width)).rotate(180,c,d)):a>90&&270>a?(this.translate(c-h.x-h.width-b-e,d-h.y-h.height/2),this.rotate(a-180,h.x+h.width+b+e,h.y+h.height/2)):(this.translate(c-h.x+b+e,d-h.y-h.height/2),this.rotate(a,h.x-b-e,h.y+h.height/2)),g.insertBefore(this.node?this:this[0])}},Raphael.el.drop=function(a,b,c){var f,g,h,i,j,d=this.getBBox(),e=this.paper||this[0].paper;if(e){switch(this.type){case"text":case"circle":case"ellipse":f=!0;break;default:f=!1}return a=a||0,b="number"==typeof b?b:f?d.x+d.width/2:d.x,c="number"==typeof c?c:f?d.y+d.height/2:d.y,g=Math.max(d.width,d.height)+Math.min(d.width,d.height),h=e.path(["M",b,c,"l",g,0,"A",.4*g,.4*g,0,1,0,b+.7*g,c-.7*g,"z"]).attr({fill:"#000",stroke:"none"}).rotate(22.5-a,b,c),a=(a+90)*Math.PI/180,i=b+g*Math.sin(a)-(f?0:d.width/2),j=c+g*Math.cos(a)-(f?0:d.height/2),this.attrs?this.attr(this.attrs.x?"x":"cx",i).attr(this.attrs.y?"y":"cy",j):this.translate(i-d.x,j-d.y),h.insertBefore(this.node?this:this[0])}},Raphael.el.flag=function(a,b,c){var d=3,e=this.paper||this[0].paper;if(e){var i,f=e.path().attr({fill:"#000",stroke:"#000"}),g=this.getBBox(),h=g.height/2;switch(this.type){case"text":case"circle":case"ellipse":i=!0;break;default:i=!1}return a=a||0,b="number"==typeof b?b:i?g.x+g.width/2:g.x,c="number"==typeof c?c:i?g.y+g.height/2:g.y,f.attr({path:["M",b,c,"l",h+d,-h-d,g.width+2*d,0,0,g.height+2*d,-g.width-2*d,0,"z"].join(",")}),a=360-a,f.rotate(a,b,c),this.attrs?(this.attr(this.attrs.x?"x":"cx",b+h+d+(i?g.width/2:"text"==this.type?g.width:0)).attr("y",i?c:c-g.height/2),this.rotate(a,b,c),a>90&&270>a&&this.attr(this.attrs.x?"x":"cx",b-h-d-(i?g.width/2:g.width)).rotate(180,b,c)):a>90&&270>a?(this.translate(b-g.x-g.width-h-d,c-g.y-g.height/2),this.rotate(a-180,g.x+g.width+h+d,g.y+g.height/2)):(this.translate(b-g.x+h+d,c-g.y-g.height/2),this.rotate(a,g.x-h-d,g.y+g.height/2)),f.insertBefore(this.node?this:this[0])}},Raphael.el.label=function(){var a=this.getBBox(),b=this.paper||this[0].paper,c=Math.min(20,a.width+10,a.height+10)/2;if(b)return b.rect(a.x-c/2,a.y-c/2,a.width+c,a.height+c,c).attr({stroke:"none",fill:"#000"}).insertBefore(this.node?this:this[0])},Raphael.el.blob=function(a,b,c){var g,h,i,d=this.getBBox(),e=Math.PI/180,f=this.paper||this[0].paper;if(f){switch(this.type){case"text":case"circle":case"ellipse":h=!0;break;default:h=!1}g=f.path().attr({fill:"#000",stroke:"none"}),a=(+a+1?a:45)+90,i=Math.min(d.height,d.width),b="number"==typeof b?b:h?d.x+d.width/2:d.x,c="number"==typeof c?c:h?d.y+d.height/2:d.y;var j=Math.max(d.width+i,25*i/12),k=Math.max(d.height+i,25*i/12),l=b+i*Math.sin((a-22.5)*e),m=c+i*Math.cos((a-22.5)*e),n=b+i*Math.sin((a+22.5)*e),o=c+i*Math.cos((a+22.5)*e),p=(n-l)/2,q=(o-m)/2,r=j/2,s=k/2,t=-Math.sqrt(Math.abs(r*r*s*s-r*r*q*q-s*s*p*p)/(r*r*q*q+s*s*p*p)),u=t*r*q/s+(n+l)/2,v=t*-s*p/r+(o+m)/2;return g.attr({x:u,y:v,path:["M",b,c,"L",n,o,"A",r,s,0,1,1,l,m,"z"].join(",")}),this.translate(u-d.x-d.width/2,v-d.y-d.height/2),g.insertBefore(this.node?this:this[0])}},Raphael.fn.label=function(a,b,c){var d=this.set();return c=this.text(a,b,c).attr(Raphael.g.txtattr),d.push(c.label(),c)},Raphael.fn.popup=function(a,b,c,d,e){var f=this.set();return c=this.text(a,b,c).attr(Raphael.g.txtattr),f.push(c.popup(d,e),c)},Raphael.fn.tag=function(a,b,c,d,e){var f=this.set();return c=this.text(a,b,c).attr(Raphael.g.txtattr),f.push(c.tag(d,e),c)},Raphael.fn.flag=function(a,b,c,d){var e=this.set();return c=this.text(a,b,c).attr(Raphael.g.txtattr),e.push(c.flag(d),c)},Raphael.fn.drop=function(a,b,c,d){var e=this.set();return c=this.text(a,b,c).attr(Raphael.g.txtattr),e.push(c.drop(d),c)},Raphael.fn.blob=function(a,b,c,d){var e=this.set();return c=this.text(a,b,c).attr(Raphael.g.txtattr),e.push(c.blob(d),c)},Raphael.el.lighter=function(a){a=a||2;var b=[this.attrs.fill,this.attrs.stroke];return this.fs=this.fs||[b[0],b[1]],b[0]=Raphael.rgb2hsb(Raphael.getRGB(b[0]).hex),b[1]=Raphael.rgb2hsb(Raphael.getRGB(b[1]).hex),b[0].b=Math.min(b[0].b*a,1),b[0].s=b[0].s/a,b[1].b=Math.min(b[1].b*a,1),b[1].s=b[1].s/a,this.attr({fill:"hsb("+[b[0].h,b[0].s,b[0].b]+")",stroke:"hsb("+[b[1].h,b[1].s,b[1].b]+")"}),this},Raphael.el.darker=function(a){a=a||2;var b=[this.attrs.fill,this.attrs.stroke];return this.fs=this.fs||[b[0],b[1]],b[0]=Raphael.rgb2hsb(Raphael.getRGB(b[0]).hex),b[1]=Raphael.rgb2hsb(Raphael.getRGB(b[1]).hex),b[0].s=Math.min(b[0].s*a,1),b[0].b=b[0].b/a,b[1].s=Math.min(b[1].s*a,1),b[1].b=b[1].b/a,this.attr({fill:"hsb("+[b[0].h,b[0].s,b[0].b]+")",stroke:"hsb("+[b[1].h,b[1].s,b[1].b]+")"}),this},Raphael.el.resetBrightness=function(){return this.fs&&(this.attr({fill:this.fs[0],stroke:this.fs[1]}),delete this.fs),this},function(){var a=["lighter","darker","resetBrightness"],b=["popup","tag","flag","label","drop","blob"];for(var c in b)(function(a){Raphael.st[a]=function(){return Raphael.el[a].apply(this,arguments)}})(b[c]);for(var c in a)(function(a){Raphael.st[a]=function(){for(var b=0;this.length>b;b++)this[b][a].apply(this[b],arguments);return this}})(a[c])}(),Raphael.g={shim:{stroke:"none",fill:"#000","fill-opacity":0},txtattr:{font:"12px Arial, sans-serif",fill:"#fff"},colors:function(){for(var a=[.6,.2,.05,.1333,.75,0],b=[],c=0;10>c;c++)a.length>c?b.push("hsb("+a[c]+",.75, .75)"):b.push("hsb("+a[c-a.length]+", 1, .5)");return b}(),snapEnds:function(a,b,c){function f(a){return.25>Math.abs(a-.5)?~~a+.5:Math.round(a)}var d=a,e=b;if(d==e)return{from:d,to:e,power:0};var g=(e-d)/c,h=~~g,i=h,j=0;if(h){for(;i;)j--,i=~~(g*Math.pow(10,j))/Math.pow(10,j);j++}else{if(0!=g&&isFinite(g))for(;!h;)j=j||1,h=~~(g*Math.pow(10,j))/Math.pow(10,j),j++;else j=1;j&&j--}return e=f(b*Math.pow(10,j))/Math.pow(10,j),b>e&&(e=f((b+.5)*Math.pow(10,j))/Math.pow(10,j)),d=f((a-(j>0?0:.5))*Math.pow(10,j))/Math.pow(10,j),{from:d,to:e,power:j}},axis:function(a,b,c,d,e,f,g,h,i,j,k){j=null==j?2:j,i=i||"t",f=f||10,k=arguments[arguments.length-1];var t,l="|"==i||" "==i?["M",a+.5,b,"l",0,.001]:1==g||3==g?["M",a+.5,b,"l",0,-c]:["M",a,b+.5,"l",c,0],m=this.snapEnds(d,e,f),n=m.from,o=m.to,p=m.power,q=0,r={font:"11px 'Fontin Sans', Fontin-Sans, sans-serif"},s=k.set();t=(o-n)/f;var u=n,v=p>0?p:0;if(z=c/f,1==+g||3==+g){for(var w=b,x=(g-1?1:-1)*(j+3+!!(g-1));w>=b-c;)"-"!=i&&" "!=i&&(l=l.concat(["M",a-("+"==i||"|"==i?j:2*!(g-1)*j),w+.5,"l",2*j+1,0])),s.push(k.text(a+x,w,h&&h[q++]||(Math.round(u)==u?u:+u.toFixed(v))).attr(r).attr({"text-anchor":g-1?"start":"end"})),u+=t,w-=z;Math.round(w+z-(b-c))&&("-"!=i&&" "!=i&&(l=l.concat(["M",a-("+"==i||"|"==i?j:2*!(g-1)*j),b-c+.5,"l",2*j+1,0])),s.push(k.text(a+x,b-c,h&&h[q]||(Math.round(u)==u?u:+u.toFixed(v))).attr(r).attr({"text-anchor":g-1?"start":"end"})))}else{u=n,v=(p>0)*p,x=(g?-1:1)*(j+9+!g);for(var y=a,z=c/f,A=0,B=0;a+c>=y;){"-"!=i&&" "!=i&&(l=l.concat(["M",y+.5,b-("+"==i?j:2*!!g*j),"l",0,2*j+1])),s.push(A=k.text(y,b+x,h&&h[q++]||(Math.round(u)==u?u:+u.toFixed(v))).attr(r));var C=A.getBBox();B>=C.x-5?s.pop(s.length-1).remove():B=C.x+C.width,u+=t,y+=z}Math.round(y-z-a-c)&&("-"!=i&&" "!=i&&(l=l.concat(["M",a+c+.5,b-("+"==i?j:2*!!g*j),"l",0,2*j+1])),s.push(k.text(a+c,b+x,h&&h[q]||(Math.round(u)==u?u:+u.toFixed(v))).attr(r)))}var D=k.path(l);return D.text=s,D.all=k.set([D,s]),D.remove=function(){this.text.remove(),this.constructor.prototype.remove.call(this)},D},labelise:function(a,b,c){return a?(a+"").replace(/(##+(?:\.#+)?)|(%%+(?:\.%+)?)/g,function(a,d,e){return d?(+b).toFixed(d.replace(/^#+\.?/g,"").length):e?(100*b/c).toFixed(e.replace(/^%+\.?/g,"").length)+"%":void 0}):(+b).toFixed(0)}}; \ No newline at end of file diff --git a/vendor/assets/javascripts/g.raphael.js b/vendor/assets/javascripts/g.raphael.js new file mode 100644 index 00000000000..27f27caf9f2 --- /dev/null +++ b/vendor/assets/javascripts/g.raphael.js @@ -0,0 +1,861 @@ +/*! + * g.Raphael 0.51 - Charting library, based on Raphaël + * + * Copyright (c) 2009-2012 Dmitry Baranovskiy (http://g.raphaeljs.com) + * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license. + */ + +/* + * Tooltips on Element prototype + */ +/*\ + * Element.popup + [ method ] + ** + * Puts the context Element in a 'popup' tooltip. Can also be used on sets. + ** + > Parameters + ** + - dir (string) location of Element relative to the tail: `'down'`, `'left'`, `'up'` [default], or `'right'`. + - size (number) amount of bevel/padding around the Element, as well as half the width and height of the tail [default: `5`] + - x (number) x coordinate of the popup's tail [default: Element's `x` or `cx`] + - y (number) y coordinate of the popup's tail [default: Element's `y` or `cy`] + ** + = (object) path element of the popup + \*/ +Raphael.el.popup = function (dir, size, x, y) { + var paper = this.paper || this[0].paper, + bb, xy, center, cw, ch; + + if (!paper) return; + + switch (this.type) { + case 'text': + case 'circle': + case 'ellipse': center = true; break; + default: center = false; + } + + dir = dir == null ? 'up' : dir; + size = size || 5; + bb = this.getBBox(); + + x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x); + y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y); + cw = Math.max(bb.width / 2 - size, 0); + ch = Math.max(bb.height / 2 - size, 0); + + this.translate(x - bb.x - (center ? bb.width / 2 : 0), y - bb.y - (center ? bb.height / 2 : 0)); + bb = this.getBBox(); + + var paths = { + up: [ + 'M', x, y, + 'l', -size, -size, -cw, 0, + 'a', size, size, 0, 0, 1, -size, -size, + 'l', 0, -bb.height, + 'a', size, size, 0, 0, 1, size, -size, + 'l', size * 2 + cw * 2, 0, + 'a', size, size, 0, 0, 1, size, size, + 'l', 0, bb.height, + 'a', size, size, 0, 0, 1, -size, size, + 'l', -cw, 0, + 'z' + ].join(','), + down: [ + 'M', x, y, + 'l', size, size, cw, 0, + 'a', size, size, 0, 0, 1, size, size, + 'l', 0, bb.height, + 'a', size, size, 0, 0, 1, -size, size, + 'l', -(size * 2 + cw * 2), 0, + 'a', size, size, 0, 0, 1, -size, -size, + 'l', 0, -bb.height, + 'a', size, size, 0, 0, 1, size, -size, + 'l', cw, 0, + 'z' + ].join(','), + left: [ + 'M', x, y, + 'l', -size, size, 0, ch, + 'a', size, size, 0, 0, 1, -size, size, + 'l', -bb.width, 0, + 'a', size, size, 0, 0, 1, -size, -size, + 'l', 0, -(size * 2 + ch * 2), + 'a', size, size, 0, 0, 1, size, -size, + 'l', bb.width, 0, + 'a', size, size, 0, 0, 1, size, size, + 'l', 0, ch, + 'z' + ].join(','), + right: [ + 'M', x, y, + 'l', size, -size, 0, -ch, + 'a', size, size, 0, 0, 1, size, -size, + 'l', bb.width, 0, + 'a', size, size, 0, 0, 1, size, size, + 'l', 0, size * 2 + ch * 2, + 'a', size, size, 0, 0, 1, -size, size, + 'l', -bb.width, 0, + 'a', size, size, 0, 0, 1, -size, -size, + 'l', 0, -ch, + 'z' + ].join(',') + }; + + xy = { + up: { x: -!center * (bb.width / 2), y: -size * 2 - (center ? bb.height / 2 : bb.height) }, + down: { x: -!center * (bb.width / 2), y: size * 2 + (center ? bb.height / 2 : bb.height) }, + left: { x: -size * 2 - (center ? bb.width / 2 : bb.width), y: -!center * (bb.height / 2) }, + right: { x: size * 2 + (center ? bb.width / 2 : bb.width), y: -!center * (bb.height / 2) } + }[dir]; + + this.translate(xy.x, xy.y); + return paper.path(paths[dir]).attr({ fill: "#000", stroke: "none" }).insertBefore(this.node ? this : this[0]); +}; + +/*\ + * Element.tag + [ method ] + ** + * Puts the context Element in a 'tag' tooltip. Can also be used on sets. + ** + > Parameters + ** + - angle (number) angle of orientation in degrees [default: `0`] + - r (number) radius of the loop [default: `5`] + - x (number) x coordinate of the center of the tag loop [default: Element's `x` or `cx`] + - y (number) y coordinate of the center of the tag loop [default: Element's `x` or `cx`] + ** + = (object) path element of the tag + \*/ +Raphael.el.tag = function (angle, r, x, y) { + var d = 3, + paper = this.paper || this[0].paper; + + if (!paper) return; + + var p = paper.path().attr({ fill: '#000', stroke: '#000' }), + bb = this.getBBox(), + dx, R, center, tmp; + + switch (this.type) { + case 'text': + case 'circle': + case 'ellipse': center = true; break; + default: center = false; + } + + angle = angle || 0; + x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x); + y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y); + r = r == null ? 5 : r; + R = .5522 * r; + + if (bb.height >= r * 2) { + p.attr({ + path: [ + "M", x, y + r, + "a", r, r, 0, 1, 1, 0, -r * 2, r, r, 0, 1, 1, 0, r * 2, + "m", 0, -r * 2 -d, + "a", r + d, r + d, 0, 1, 0, 0, (r + d) * 2, + "L", x + r + d, y + bb.height / 2 + d, + "l", bb.width + 2 * d, 0, 0, -bb.height - 2 * d, -bb.width - 2 * d, 0, + "L", x, y - r - d + ].join(",") + }); + } else { + dx = Math.sqrt(Math.pow(r + d, 2) - Math.pow(bb.height / 2 + d, 2)); + p.attr({ + path: [ + "M", x, y + r, + "c", -R, 0, -r, R - r, -r, -r, 0, -R, r - R, -r, r, -r, R, 0, r, r - R, r, r, 0, R, R - r, r, -r, r, + "M", x + dx, y - bb.height / 2 - d, + "a", r + d, r + d, 0, 1, 0, 0, bb.height + 2 * d, + "l", r + d - dx + bb.width + 2 * d, 0, 0, -bb.height - 2 * d, + "L", x + dx, y - bb.height / 2 - d + ].join(",") + }); + } + + angle = 360 - angle; + p.rotate(angle, x, y); + + if (this.attrs) { + //elements + this.attr(this.attrs.x ? 'x' : 'cx', x + r + d + (!center ? this.type == 'text' ? bb.width : 0 : bb.width / 2)).attr('y', center ? y : y - bb.height / 2); + this.rotate(angle, x, y); + angle > 90 && angle < 270 && this.attr(this.attrs.x ? 'x' : 'cx', x - r - d - (!center ? bb.width : bb.width / 2)).rotate(180, x, y); + } else { + //sets + if (angle > 90 && angle < 270) { + this.translate(x - bb.x - bb.width - r - d, y - bb.y - bb.height / 2); + this.rotate(angle - 180, bb.x + bb.width + r + d, bb.y + bb.height / 2); + } else { + this.translate(x - bb.x + r + d, y - bb.y - bb.height / 2); + this.rotate(angle, bb.x - r - d, bb.y + bb.height / 2); + } + } + + return p.insertBefore(this.node ? this : this[0]); +}; + +/*\ + * Element.drop + [ method ] + ** + * Puts the context Element in a 'drop' tooltip. Can also be used on sets. + ** + > Parameters + ** + - angle (number) angle of orientation in degrees [default: `0`] + - x (number) x coordinate of the drop's point [default: Element's `x` or `cx`] + - y (number) y coordinate of the drop's point [default: Element's `x` or `cx`] + ** + = (object) path element of the drop + \*/ +Raphael.el.drop = function (angle, x, y) { + var bb = this.getBBox(), + paper = this.paper || this[0].paper, + center, size, p, dx, dy; + + if (!paper) return; + + switch (this.type) { + case 'text': + case 'circle': + case 'ellipse': center = true; break; + default: center = false; + } + + angle = angle || 0; + + x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x); + y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y); + size = Math.max(bb.width, bb.height) + Math.min(bb.width, bb.height); + p = paper.path([ + "M", x, y, + "l", size, 0, + "A", size * .4, size * .4, 0, 1, 0, x + size * .7, y - size * .7, + "z" + ]).attr({fill: "#000", stroke: "none"}).rotate(22.5 - angle, x, y); + + angle = (angle + 90) * Math.PI / 180; + dx = (x + size * Math.sin(angle)) - (center ? 0 : bb.width / 2); + dy = (y + size * Math.cos(angle)) - (center ? 0 : bb.height / 2); + + this.attrs ? + this.attr(this.attrs.x ? 'x' : 'cx', dx).attr(this.attrs.y ? 'y' : 'cy', dy) : + this.translate(dx - bb.x, dy - bb.y); + + return p.insertBefore(this.node ? this : this[0]); +}; + +/*\ + * Element.flag + [ method ] + ** + * Puts the context Element in a 'flag' tooltip. Can also be used on sets. + ** + > Parameters + ** + - angle (number) angle of orientation in degrees [default: `0`] + - x (number) x coordinate of the flag's point [default: Element's `x` or `cx`] + - y (number) y coordinate of the flag's point [default: Element's `x` or `cx`] + ** + = (object) path element of the flag + \*/ +Raphael.el.flag = function (angle, x, y) { + var d = 3, + paper = this.paper || this[0].paper; + + if (!paper) return; + + var p = paper.path().attr({ fill: '#000', stroke: '#000' }), + bb = this.getBBox(), + h = bb.height / 2, + center; + + switch (this.type) { + case 'text': + case 'circle': + case 'ellipse': center = true; break; + default: center = false; + } + + angle = angle || 0; + x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x); + y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2: bb.y); + + p.attr({ + path: [ + "M", x, y, + "l", h + d, -h - d, bb.width + 2 * d, 0, 0, bb.height + 2 * d, -bb.width - 2 * d, 0, + "z" + ].join(",") + }); + + angle = 360 - angle; + p.rotate(angle, x, y); + + if (this.attrs) { + //elements + this.attr(this.attrs.x ? 'x' : 'cx', x + h + d + (!center ? this.type == 'text' ? bb.width : 0 : bb.width / 2)).attr('y', center ? y : y - bb.height / 2); + this.rotate(angle, x, y); + angle > 90 && angle < 270 && this.attr(this.attrs.x ? 'x' : 'cx', x - h - d - (!center ? bb.width : bb.width / 2)).rotate(180, x, y); + } else { + //sets + if (angle > 90 && angle < 270) { + this.translate(x - bb.x - bb.width - h - d, y - bb.y - bb.height / 2); + this.rotate(angle - 180, bb.x + bb.width + h + d, bb.y + bb.height / 2); + } else { + this.translate(x - bb.x + h + d, y - bb.y - bb.height / 2); + this.rotate(angle, bb.x - h - d, bb.y + bb.height / 2); + } + } + + return p.insertBefore(this.node ? this : this[0]); +}; + +/*\ + * Element.label + [ method ] + ** + * Puts the context Element in a 'label' tooltip. Can also be used on sets. + ** + = (object) path element of the label. + \*/ +Raphael.el.label = function () { + var bb = this.getBBox(), + paper = this.paper || this[0].paper, + r = Math.min(20, bb.width + 10, bb.height + 10) / 2; + + if (!paper) return; + + return paper.rect(bb.x - r / 2, bb.y - r / 2, bb.width + r, bb.height + r, r).attr({ stroke: 'none', fill: '#000' }).insertBefore(this.node ? this : this[0]); +}; + +/*\ + * Element.blob + [ method ] + ** + * Puts the context Element in a 'blob' tooltip. Can also be used on sets. + ** + > Parameters + ** + - angle (number) angle of orientation in degrees [default: `0`] + - x (number) x coordinate of the blob's tail [default: Element's `x` or `cx`] + - y (number) y coordinate of the blob's tail [default: Element's `x` or `cx`] + ** + = (object) path element of the blob + \*/ +Raphael.el.blob = function (angle, x, y) { + var bb = this.getBBox(), + rad = Math.PI / 180, + paper = this.paper || this[0].paper, + p, center, size; + + if (!paper) return; + + switch (this.type) { + case 'text': + case 'circle': + case 'ellipse': center = true; break; + default: center = false; + } + + p = paper.path().attr({ fill: "#000", stroke: "none" }); + angle = (+angle + 1 ? angle : 45) + 90; + size = Math.min(bb.height, bb.width); + x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x); + y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y); + + var w = Math.max(bb.width + size, size * 25 / 12), + h = Math.max(bb.height + size, size * 25 / 12), + x2 = x + size * Math.sin((angle - 22.5) * rad), + y2 = y + size * Math.cos((angle - 22.5) * rad), + x1 = x + size * Math.sin((angle + 22.5) * rad), + y1 = y + size * Math.cos((angle + 22.5) * rad), + dx = (x1 - x2) / 2, + dy = (y1 - y2) / 2, + rx = w / 2, + ry = h / 2, + k = -Math.sqrt(Math.abs(rx * rx * ry * ry - rx * rx * dy * dy - ry * ry * dx * dx) / (rx * rx * dy * dy + ry * ry * dx * dx)), + cx = k * rx * dy / ry + (x1 + x2) / 2, + cy = k * -ry * dx / rx + (y1 + y2) / 2; + + p.attr({ + x: cx, + y: cy, + path: [ + "M", x, y, + "L", x1, y1, + "A", rx, ry, 0, 1, 1, x2, y2, + "z" + ].join(",") + }); + + this.translate(cx - bb.x - bb.width / 2, cy - bb.y - bb.height / 2); + + return p.insertBefore(this.node ? this : this[0]); +}; + +/* + * Tooltips on Paper prototype + */ +/*\ + * Paper.label + [ method ] + ** + * Puts the given `text` into a 'label' tooltip. The text is given a default style according to @g.txtattr. See @Element.label + ** + > Parameters + ** + - x (number) x coordinate of the center of the label + - y (number) y coordinate of the center of the label + - text (string) text to place inside the label + ** + = (object) set containing the label path and the text element + > Usage + | paper.label(50, 50, "$9.99"); + \*/ +Raphael.fn.label = function (x, y, text) { + var set = this.set(); + + text = this.text(x, y, text).attr(Raphael.g.txtattr); + return set.push(text.label(), text); +}; + +/*\ + * Paper.popup + [ method ] + ** + * Puts the given `text` into a 'popup' tooltip. The text is given a default style according to @g.txtattr. See @Element.popup + * + * Note: The `dir` parameter has changed from g.Raphael 0.4.1 to 0.5. The options `0`, `1`, `2`, and `3` has been changed to `'down'`, `'left'`, `'up'`, and `'right'` respectively. + ** + > Parameters + ** + - x (number) x coordinate of the popup's tail + - y (number) y coordinate of the popup's tail + - text (string) text to place inside the popup + - dir (string) location of the text relative to the tail: `'down'`, `'left'`, `'up'` [default], or `'right'`. + - size (number) amount of padding around the Element [default: `5`] + ** + = (object) set containing the popup path and the text element + > Usage + | paper.popup(50, 50, "$9.99", 'down'); + \*/ +Raphael.fn.popup = function (x, y, text, dir, size) { + var set = this.set(); + + text = this.text(x, y, text).attr(Raphael.g.txtattr); + return set.push(text.popup(dir, size), text); +}; + +/*\ + * Paper.tag + [ method ] + ** + * Puts the given text into a 'tag' tooltip. The text is given a default style according to @g.txtattr. See @Element.tag + ** + > Parameters + ** + - x (number) x coordinate of the center of the tag loop + - y (number) y coordinate of the center of the tag loop + - text (string) text to place inside the tag + - angle (number) angle of orientation in degrees [default: `0`] + - r (number) radius of the loop [default: `5`] + ** + = (object) set containing the tag path and the text element + > Usage + | paper.tag(50, 50, "$9.99", 60); + \*/ +Raphael.fn.tag = function (x, y, text, angle, r) { + var set = this.set(); + + text = this.text(x, y, text).attr(Raphael.g.txtattr); + return set.push(text.tag(angle, r), text); +}; + +/*\ + * Paper.flag + [ method ] + ** + * Puts the given `text` into a 'flag' tooltip. The text is given a default style according to @g.txtattr. See @Element.flag + ** + > Parameters + ** + - x (number) x coordinate of the flag's point + - y (number) y coordinate of the flag's point + - text (string) text to place inside the flag + - angle (number) angle of orientation in degrees [default: `0`] + ** + = (object) set containing the flag path and the text element + > Usage + | paper.flag(50, 50, "$9.99", 60); + \*/ +Raphael.fn.flag = function (x, y, text, angle) { + var set = this.set(); + + text = this.text(x, y, text).attr(Raphael.g.txtattr); + return set.push(text.flag(angle), text); +}; + +/*\ + * Paper.drop + [ method ] + ** + * Puts the given text into a 'drop' tooltip. The text is given a default style according to @g.txtattr. See @Element.drop + ** + > Parameters + ** + - x (number) x coordinate of the drop's point + - y (number) y coordinate of the drop's point + - text (string) text to place inside the drop + - angle (number) angle of orientation in degrees [default: `0`] + ** + = (object) set containing the drop path and the text element + > Usage + | paper.drop(50, 50, "$9.99", 60); + \*/ +Raphael.fn.drop = function (x, y, text, angle) { + var set = this.set(); + + text = this.text(x, y, text).attr(Raphael.g.txtattr); + return set.push(text.drop(angle), text); +}; + +/*\ + * Paper.blob + [ method ] + ** + * Puts the given text into a 'blob' tooltip. The text is given a default style according to @g.txtattr. See @Element.blob + ** + > Parameters + ** + - x (number) x coordinate of the blob's tail + - y (number) y coordinate of the blob's tail + - text (string) text to place inside the blob + - angle (number) angle of orientation in degrees [default: `0`] + ** + = (object) set containing the blob path and the text element + > Usage + | paper.blob(50, 50, "$9.99", 60); + \*/ +Raphael.fn.blob = function (x, y, text, angle) { + var set = this.set(); + + text = this.text(x, y, text).attr(Raphael.g.txtattr); + return set.push(text.blob(angle), text); +}; + +/** + * Brightness functions on the Element prototype + */ +/*\ + * Element.lighter + [ method ] + ** + * Makes the context element lighter by increasing the brightness and reducing the saturation by a given factor. Can be called on Sets. + ** + > Parameters + ** + - times (number) adjustment factor [default: `2`] + ** + = (object) Element + > Usage + | paper.circle(50, 50, 20).attr({ + | fill: "#ff0000", + | stroke: "#fff", + | "stroke-width": 2 + | }).lighter(6); + \*/ +Raphael.el.lighter = function (times) { + times = times || 2; + + var fs = [this.attrs.fill, this.attrs.stroke]; + + this.fs = this.fs || [fs[0], fs[1]]; + + fs[0] = Raphael.rgb2hsb(Raphael.getRGB(fs[0]).hex); + fs[1] = Raphael.rgb2hsb(Raphael.getRGB(fs[1]).hex); + fs[0].b = Math.min(fs[0].b * times, 1); + fs[0].s = fs[0].s / times; + fs[1].b = Math.min(fs[1].b * times, 1); + fs[1].s = fs[1].s / times; + + this.attr({fill: "hsb(" + [fs[0].h, fs[0].s, fs[0].b] + ")", stroke: "hsb(" + [fs[1].h, fs[1].s, fs[1].b] + ")"}); + return this; +}; + +/*\ + * Element.darker + [ method ] + ** + * Makes the context element darker by decreasing the brightness and increasing the saturation by a given factor. Can be called on Sets. + ** + > Parameters + ** + - times (number) adjustment factor [default: `2`] + ** + = (object) Element + > Usage + | paper.circle(50, 50, 20).attr({ + | fill: "#ff0000", + | stroke: "#fff", + | "stroke-width": 2 + | }).darker(6); + \*/ +Raphael.el.darker = function (times) { + times = times || 2; + + var fs = [this.attrs.fill, this.attrs.stroke]; + + this.fs = this.fs || [fs[0], fs[1]]; + + fs[0] = Raphael.rgb2hsb(Raphael.getRGB(fs[0]).hex); + fs[1] = Raphael.rgb2hsb(Raphael.getRGB(fs[1]).hex); + fs[0].s = Math.min(fs[0].s * times, 1); + fs[0].b = fs[0].b / times; + fs[1].s = Math.min(fs[1].s * times, 1); + fs[1].b = fs[1].b / times; + + this.attr({fill: "hsb(" + [fs[0].h, fs[0].s, fs[0].b] + ")", stroke: "hsb(" + [fs[1].h, fs[1].s, fs[1].b] + ")"}); + return this; +}; + +/*\ + * Element.resetBrightness + [ method ] + ** + * Resets brightness and saturation levels to their original values. See @Element.lighter and @Element.darker. Can be called on Sets. + ** + = (object) Element + > Usage + | paper.circle(50, 50, 20).attr({ + | fill: "#ff0000", + | stroke: "#fff", + | "stroke-width": 2 + | }).lighter(6).resetBrightness(); + \*/ +Raphael.el.resetBrightness = function () { + if (this.fs) { + this.attr({ fill: this.fs[0], stroke: this.fs[1] }); + delete this.fs; + } + return this; +}; + +//alias to set prototype +(function () { + var brightness = ['lighter', 'darker', 'resetBrightness'], + tooltips = ['popup', 'tag', 'flag', 'label', 'drop', 'blob']; + + for (var f in tooltips) (function (name) { + Raphael.st[name] = function () { + return Raphael.el[name].apply(this, arguments); + }; + })(tooltips[f]); + + for (var f in brightness) (function (name) { + Raphael.st[name] = function () { + for (var i = 0; i < this.length; i++) { + this[i][name].apply(this[i], arguments); + } + + return this; + }; + })(brightness[f]); +})(); + +//chart prototype for storing common functions +Raphael.g = { + /*\ + * g.shim + [ object ] + ** + * An attribute object that charts will set on all generated shims (shims being the invisible objects that mouse events are bound to) + ** + > Default value + | { stroke: 'none', fill: '#000', 'fill-opacity': 0 } + \*/ + shim: { stroke: 'none', fill: '#000', 'fill-opacity': 0 }, + + /*\ + * g.txtattr + [ object ] + ** + * An attribute object that charts and tooltips will set on any generated text + ** + > Default value + | { font: '12px Arial, sans-serif', fill: '#fff' } + \*/ + txtattr: { font: '12px Arial, sans-serif', fill: '#fff' }, + + /*\ + * g.colors + [ array ] + ** + * An array of color values that charts will iterate through when drawing chart data values. + ** + \*/ + colors: (function () { + var hues = [.6, .2, .05, .1333, .75, 0], + colors = []; + + for (var i = 0; i < 10; i++) { + if (i < hues.length) { + colors.push('hsb(' + hues[i] + ',.75, .75)'); + } else { + colors.push('hsb(' + hues[i - hues.length] + ', 1, .5)'); + } + } + + return colors; + })(), + + snapEnds: function(from, to, steps) { + var f = from, + t = to; + + if (f == t) { + return {from: f, to: t, power: 0}; + } + + function round(a) { + return Math.abs(a - .5) < .25 ? ~~(a) + .5 : Math.round(a); + } + + var d = (t - f) / steps, + r = ~~(d), + R = r, + i = 0; + + if (r) { + while (R) { + i--; + R = ~~(d * Math.pow(10, i)) / Math.pow(10, i); + } + + i ++; + } else { + if(d == 0 || !isFinite(d)) { + i = 1; + } else { + while (!r) { + i = i || 1; + r = ~~(d * Math.pow(10, i)) / Math.pow(10, i); + i++; + } + } + + i && i--; + } + + t = round(to * Math.pow(10, i)) / Math.pow(10, i); + + if (t < to) { + t = round((to + .5) * Math.pow(10, i)) / Math.pow(10, i); + } + + f = round((from - (i > 0 ? 0 : .5)) * Math.pow(10, i)) / Math.pow(10, i); + return { from: f, to: t, power: i }; + }, + + axis: function (x, y, length, from, to, steps, orientation, labels, type, dashsize, paper) { + dashsize = dashsize == null ? 2 : dashsize; + type = type || "t"; + steps = steps || 10; + paper = arguments[arguments.length-1] //paper is always last argument + + var path = type == "|" || type == " " ? ["M", x + .5, y, "l", 0, .001] : orientation == 1 || orientation == 3 ? ["M", x + .5, y, "l", 0, -length] : ["M", x, y + .5, "l", length, 0], + ends = this.snapEnds(from, to, steps), + f = ends.from, + t = ends.to, + i = ends.power, + j = 0, + txtattr = { font: "11px 'Fontin Sans', Fontin-Sans, sans-serif" }, + text = paper.set(), + d; + + d = (t - f) / steps; + + var label = f, + rnd = i > 0 ? i : 0; + dx = length / steps; + + if (+orientation == 1 || +orientation == 3) { + var Y = y, + addon = (orientation - 1 ? 1 : -1) * (dashsize + 3 + !!(orientation - 1)); + + while (Y >= y - length) { + type != "-" && type != " " && (path = path.concat(["M", x - (type == "+" || type == "|" ? dashsize : !(orientation - 1) * dashsize * 2), Y + .5, "l", dashsize * 2 + 1, 0])); + text.push(paper.text(x + addon, Y, (labels && labels[j++]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr).attr({ "text-anchor": orientation - 1 ? "start" : "end" })); + label += d; + Y -= dx; + } + + if (Math.round(Y + dx - (y - length))) { + type != "-" && type != " " && (path = path.concat(["M", x - (type == "+" || type == "|" ? dashsize : !(orientation - 1) * dashsize * 2), y - length + .5, "l", dashsize * 2 + 1, 0])); + text.push(paper.text(x + addon, y - length, (labels && labels[j]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr).attr({ "text-anchor": orientation - 1 ? "start" : "end" })); + } + } else { + label = f; + rnd = (i > 0) * i; + addon = (orientation ? -1 : 1) * (dashsize + 9 + !orientation); + + var X = x, + dx = length / steps, + txt = 0, + prev = 0; + + while (X <= x + length) { + type != "-" && type != " " && (path = path.concat(["M", X + .5, y - (type == "+" ? dashsize : !!orientation * dashsize * 2), "l", 0, dashsize * 2 + 1])); + text.push(txt = paper.text(X, y + addon, (labels && labels[j++]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr)); + + var bb = txt.getBBox(); + + if (prev >= bb.x - 5) { + text.pop(text.length - 1).remove(); + } else { + prev = bb.x + bb.width; + } + + label += d; + X += dx; + } + + if (Math.round(X - dx - x - length)) { + type != "-" && type != " " && (path = path.concat(["M", x + length + .5, y - (type == "+" ? dashsize : !!orientation * dashsize * 2), "l", 0, dashsize * 2 + 1])); + text.push(paper.text(x + length, y + addon, (labels && labels[j]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr)); + } + } + + var res = paper.path(path); + + res.text = text; + res.all = paper.set([res, text]); + res.remove = function () { + this.text.remove(); + this.constructor.prototype.remove.call(this); + }; + + return res; + }, + + labelise: function(label, val, total) { + if (label) { + return (label + "").replace(/(##+(?:\.#+)?)|(%%+(?:\.%+)?)/g, function (all, value, percent) { + if (value) { + return (+val).toFixed(value.replace(/^#+\.?/g, "").length); + } + if (percent) { + return (val * 100 / total).toFixed(percent.replace(/^%+\.?/g, "").length) + "%"; + } + }); + } else { + return (+val).toFixed(0); + } + } +} -- cgit v1.2.1 From f3c33b3303e0f83e975f58ff0a3348bffbd10928 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 19 Jan 2016 19:49:11 -0200 Subject: Don't vendor minified g.bar.js --- app/assets/javascripts/application.js.coffee | 2 +- vendor/assets/javascripts/g.bar-min.js | 8 - vendor/assets/javascripts/g.bar.js | 674 +++++++++++++++++++++++++++ 3 files changed, 675 insertions(+), 9 deletions(-) delete mode 100644 vendor/assets/javascripts/g.bar-min.js create mode 100644 vendor/assets/javascripts/g.bar.js diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 1033c383550..981400a7c31 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -22,7 +22,7 @@ #= require select2 #= require raphael #= require g.raphael -#= require g.bar-min +#= require g.bar #= require Chart #= require branch-graph #= require ace/ace diff --git a/vendor/assets/javascripts/g.bar-min.js b/vendor/assets/javascripts/g.bar-min.js deleted file mode 100644 index 7620dabda74..00000000000 --- a/vendor/assets/javascripts/g.bar-min.js +++ /dev/null @@ -1,8 +0,0 @@ -/*! - * g.Raphael 0.5 - Charting library, based on Raphaël - * - * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com) - * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license. - * From: https://github.com/jhurt/g.raphael/blob/master/g.bar.js - */ -(function(){function c(c,d,e,f,g,h,i,j){var k,l={round:"round",sharp:"sharp",soft:"soft",square:"square"};if(g&&!f||!g&&!e)return i?"":j.path();switch(h=l[h]||"square",f=Math.round(f),e=Math.round(e),c=Math.round(c),d=Math.round(d),h){case"round":if(g)m=~~(e/2),m>f?(m=f,k=["M",c-~~(e/2),d,"l",0,0,"a",~~(e/2),m,0,0,1,e,0,"l",0,0,"z"]):k=["M",c-m,d,"l",0,m-f,"a",m,m,0,1,1,e,0,"l",0,f-m,"z"];else{var m=~~(f/2);m>e?(m=e,k=["M",c+.5,d+.5-~~(f/2),"l",0,0,"a",m,~~(f/2),0,0,1,0,f,"l",0,0,"z"]):k=["M",c+.5,d+.5-m,"l",e-m,0,"a",m,m,0,1,1,0,f,"l",m-e,0,"z"]}break;case"sharp":if(g)n=~~(e/2),k=["M",c+n,d,"l",-e,0,0,-b(f-n,0),n,-a(n,f),n,a(n,f),n,"z"];else{var n=~~(f/2);k=["M",c,d+n,"l",0,-f,b(e-n,0),0,a(n,e),n,-a(n,e),n+(f>2*n),"z"]}break;case"square":k=g?["M",c+~~(e/2),d,"l",1-e,0,0,-f,e-1,0,"z"]:["M",c,d+~~(f/2),"l",0,-f,e,0,0,f,"z"];break;case"soft":g?(m=a(Math.round(e/5),f),k=["M",c-~~(e/2),d,"l",0,m-f,"a",m,m,0,0,1,m,-m,"l",e-2*m,0,"a",m,m,0,0,1,m,m,"l",0,f-m,"z"]):(m=a(e,Math.round(f/5)),k=["M",c+.5,d+.5-~~(f/2),"l",e-m,0,"a",m,m,0,0,1,m,m,"l",0,f-2*m,"a",m,m,0,0,1,-m,m,"l",m-e,0,"z"])}return i?k.join(","):j.path(k)}function d(a,b,d,e,f,g,h){h=h||{};var i=this,j=h.type||"square",k=parseFloat(h.gutter||"20%"),l=a.set(),m=a.set(),n=a.set(),o=a.set(),p=Math.max.apply(Math,g),q=[],r=0,s=h.colors||i.colors,t=g.length;if(Raphael.is(g[0],"array")){p=[],r=t,t=0;for(var u=g.length;u--;)m.push(a.set()),p.push(Math.max.apply(Math,g[u])),t=Math.max(t,g[u].length);if(h.stacked)for(var u=t;u--;){for(var v=0,w=g.length;w--;)v+=+g[w][u]||0;q.push(v)}for(var u=g.length;u--;)if(t>g[u].length)for(var w=t;w--;)g[u].push(0);p=Math.max.apply(Math,h.stacked?q:p)}p=h.to||p;var x=100*(e/(t*(100+k)+k)),y=x*k/100,z=null==h.vgutter?20:h.vgutter,A=[],B=b+y,C=(f-2*z)/p;h.stretch||(y=Math.round(y),x=Math.floor(x)),!h.stacked&&(x/=r||1);for(var u=0;t>u;u++){A=[];for(var w=0;(r||1)>w;w++){var D=Math.round((r?g[w][u]:g[u])*C),E=d+f-z-D,F=c(Math.round(B+x/2),E+D,x,D,!0,j,null,a).attr({stroke:"none",fill:s[r?w:u]});r?m[w].push(F):m.push(F),F.y=E,F.x=Math.round(B+x/2),F.w=x,F.h=D,F.value=r?g[w][u]:g[u],h.stacked?A.push(F):B+=x}if(h.stacked){var G;o.push(G=a.rect(A[0].x-A[0].w/2,d,x,f).attr(i.shim)),G.bars=a.set();for(var H=0,I=A.length;I--;)A[I].toFront();for(var I=0,J=A.length;J>I;I++){var K,F=A[I],D=(H+F.value)*C,L=c(F.x,d+f-z-.5*!!H,x,D,!0,j,1,a);G.bars.push(F),H&&F.attr({path:L}),F.h=D,F.y=d+f-z-.5*!!H-D,n.push(K=a.rect(F.x-F.w/2,F.y,x,F.value*C).attr(i.shim)),K.bar=F,K.value=F.value,H+=F.value}B+=x}B+=y}if(o.toFront(),B=b+y,!h.stacked)for(var u=0;t>u;u++){for(var w=0;(r||1)>w;w++){var K;n.push(K=a.rect(Math.round(B),d+z,x,f-z).attr(i.shim)),K.bar=r?m[w][u]:m[u],K.value=K.bar.value,B+=x}B+=y}return l.label=function(b,c){b=b||[],this.labels=a.set();var e,j=-1/0;if(h.stacked){for(var k=0;t>k;k++)for(var l=0,o=0;(r||1)>o;o++)if(l+=r?g[o][k]:g[k],o==r-1){var q=i.labelise(b[k],l,p);e=a.text(m[o][k].x,d+f-z/2,q).attr(i.txtattr).attr({fill:h.legendcolor||"#000","text-anchor":"start"}).insertBefore(n[k*(r||1)+o]);var s=e.getBBox();j>s.x-7?e.remove():(this.labels.push(e),j=s.x+s.width)}}else for(var k=0;t>k;k++)for(var o=0;(r||1)>o;o++){var q=i.labelise(r?b[o]&&b[o][k]:b[k],r?g[o][k]:g[k],p);e=a.text(m[o][k].x-x/2,c?d+f-z/2:m[o][k].y-10,q).attr(i.txtattr).attr({fill:h.legendcolor||"#000","text-anchor":"start"}).insertBefore(n[k*(r||1)+o]);var s=e.getBBox();e.translate((x-s.width)/2,1),j>s.x-7?e.remove():(this.labels.push(e),j=s.x+s.width)}return this},l.hover=function(a,b){return o.hide(),n.show(),n.mouseover(a).mouseout(b),this},l.hoverColumn=function(a,b){return n.hide(),o.show(),b=b||function(){},o.mouseover(a).mouseout(b),this},l.click=function(a){return o.hide(),n.show(),n.click(a),this},l.each=function(a){if(!Raphael.is(a,"function"))return this;for(var b=n.length;b--;)a.call(n[b]);return this},l.eachColumn=function(a){if(!Raphael.is(a,"function"))return this;for(var b=o.length;b--;)a.call(o[b]);return this},l.clickColumn=function(a){return n.hide(),o.show(),o.click(a),this},l.push(m,n,o),l.bars=m,l.covers=n,l}function e(a,b,d,e,f,g,h){h=h||{};var i=this,j=h.type||"square",k=parseFloat(h.gutter||"20%"),l=a.set(),m=a.set(),n=a.set(),o=a.set(),p=Math.max.apply(Math,g),q=[],r=0,s=h.colors||i.colors,t=g.length;if(Raphael.is(g[0],"array")){p=[],r=t,t=0;for(var u=g.length;u--;)m.push(a.set()),p.push(Math.max.apply(Math,g[u])),t=Math.max(t,g[u].length);if(h.stacked)for(var u=t;u--;){for(var v=0,w=g.length;w--;)v+=+g[w][u]||0;q.push(v)}for(var u=g.length;u--;)if(t>g[u].length)for(var w=t;w--;)g[u].push(0);p=Math.max.apply(Math,h.stacked?q:p)}p=h.to||p;var x=Math.floor(100*(f/(t*(100+k)+k))),y=Math.floor(x*k/100),z=[],A=d+y,B=(e-1)/p;!h.stacked&&(x/=r||1);for(var u=0;t>u;u++){z=[];for(var w=0;(r||1)>w;w++){var C=r?g[w][u]:g[u],D=c(b,A+x/2,Math.round(C*B),x-1,!1,j,null,a).attr({stroke:"none",fill:s[r?w:u]});r?m[w].push(D):m.push(D),D.x=b+Math.round(C*B),D.y=A+x/2,D.w=Math.round(C*B),D.h=x,D.value=+C,h.stacked?z.push(D):A+=x}if(h.stacked){var E=a.rect(b,z[0].y-z[0].h/2,e,x).attr(i.shim);o.push(E),E.bars=a.set();for(var F=0,G=z.length;G--;)z[G].toFront();for(var G=0,H=z.length;H>G;G++){var I,D=z[G],C=Math.round((F+D.value)*B),J=c(b,D.y,C,x-1,!1,j,1,a);E.bars.push(D),F&&D.attr({path:J}),D.w=C,D.x=b+C,n.push(I=a.rect(b+F*B,D.y-D.h/2,D.value*B,x).attr(i.shim)),I.bar=D,F+=D.value}A+=x}A+=y}if(o.toFront(),A=d+y,!h.stacked)for(var u=0;t>u;u++){for(var w=0;(r||1)>w;w++){var I=a.rect(b,A,e,x).attr(i.shim);n.push(I),I.bar=r?m[w][u]:m[u],I.value=I.bar.value,A+=x}A+=y}return l.label=function(c,d){c=c||[],this.labels=a.set();for(var e=0;t>e;e++)for(var f=0;r>f;f++){var o,j=i.labelise(r?c[f]&&c[f][e]:c[e],r?g[f][e]:g[e],p),k=d?m[f][e].x-x/2+3:b+5;this.labels.push(o=a.text(k,m[f][e].y,j).attr(i.txtattr).attr({fill:h.legendcolor||"#000","text-anchor":"start"}).insertBefore(n[0])),b+5>o.getBBox().x?o.attr({x:b+5,"text-anchor":"start"}):m[f][e].label=o}return this},l.hover=function(a,b){return o.hide(),n.show(),b=b||function(){},n.mouseover(a).mouseout(b),this},l.hoverColumn=function(a,b){return n.hide(),o.show(),b=b||function(){},o.mouseover(a).mouseout(b),this},l.each=function(a){if(!Raphael.is(a,"function"))return this;for(var b=n.length;b--;)a.call(n[b]);return this},l.eachColumn=function(a){if(!Raphael.is(a,"function"))return this;for(var b=o.length;b--;)a.call(o[b]);return this},l.click=function(a){return o.hide(),n.show(),n.click(a),this},l.clickColumn=function(a){return n.hide(),o.show(),o.click(a),this},l.push(m,n,o),l.bars=m,l.covers=n,l}var a=Math.min,b=Math.max,f=function(){};f.prototype=Raphael.g,e.prototype=d.prototype=new f,Raphael.fn.hbarchart=function(a,b,c,d,f,g){return new e(this,a,b,c,d,f,g)},Raphael.fn.barchart=function(a,b,c,e,f,g){return new d(this,a,b,c,e,f,g)}})(); \ No newline at end of file diff --git a/vendor/assets/javascripts/g.bar.js b/vendor/assets/javascripts/g.bar.js new file mode 100644 index 00000000000..166bd654d6e --- /dev/null +++ b/vendor/assets/javascripts/g.bar.js @@ -0,0 +1,674 @@ +/*! + * g.Raphael 0.51 - Charting library, based on Raphaël + * + * Copyright (c) 2009-2012 Dmitry Baranovskiy (http://g.raphaeljs.com) + * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license. + */ +(function () { + var mmin = Math.min, + mmax = Math.max; + + function finger(x, y, width, height, dir, ending, isPath, paper) { + var path, + ends = { round: 'round', sharp: 'sharp', soft: 'soft', square: 'square' }; + + // dir 0 for horizontal and 1 for vertical + if ((dir && !height) || (!dir && !width)) { + return isPath ? "" : paper.path(); + } + + ending = ends[ending] || "square"; + height = Math.round(height); + width = Math.round(width); + x = Math.round(x); + y = Math.round(y); + + switch (ending) { + case "round": + if (!dir) { + var r = ~~(height / 2); + + if (width < r) { + r = width; + path = [ + "M", x + .5, y + .5 - ~~(height / 2), + "l", 0, 0, + "a", r, ~~(height / 2), 0, 0, 1, 0, height, + "l", 0, 0, + "z" + ]; + } else { + path = [ + "M", x + .5, y + .5 - r, + "l", width - r, 0, + "a", r, r, 0, 1, 1, 0, height, + "l", r - width, 0, + "z" + ]; + } + } else { + r = ~~(width / 2); + + if (height < r) { + r = height; + path = [ + "M", x - ~~(width / 2), y, + "l", 0, 0, + "a", ~~(width / 2), r, 0, 0, 1, width, 0, + "l", 0, 0, + "z" + ]; + } else { + path = [ + "M", x - r, y, + "l", 0, r - height, + "a", r, r, 0, 1, 1, width, 0, + "l", 0, height - r, + "z" + ]; + } + } + break; + case "sharp": + if (!dir) { + var half = ~~(height / 2); + + path = [ + "M", x, y + half, + "l", 0, -height, mmax(width - half, 0), 0, mmin(half, width), half, -mmin(half, width), half + (half * 2 < height), + "z" + ]; + } else { + half = ~~(width / 2); + path = [ + "M", x + half, y, + "l", -width, 0, 0, -mmax(height - half, 0), half, -mmin(half, height), half, mmin(half, height), half, + "z" + ]; + } + break; + case "square": + if (!dir) { + path = [ + "M", x, y + ~~(height / 2), + "l", 0, -height, width, 0, 0, height, + "z" + ]; + } else { + path = [ + "M", x + ~~(width / 2), y, + "l", 1 - width, 0, 0, -height, width - 1, 0, + "z" + ]; + } + break; + case "soft": + if (!dir) { + r = mmin(width, Math.round(height / 5)); + path = [ + "M", x + .5, y + .5 - ~~(height / 2), + "l", width - r, 0, + "a", r, r, 0, 0, 1, r, r, + "l", 0, height - r * 2, + "a", r, r, 0, 0, 1, -r, r, + "l", r - width, 0, + "z" + ]; + } else { + r = mmin(Math.round(width / 5), height); + path = [ + "M", x - ~~(width / 2), y, + "l", 0, r - height, + "a", r, r, 0, 0, 1, r, -r, + "l", width - 2 * r, 0, + "a", r, r, 0, 0, 1, r, r, + "l", 0, height - r, + "z" + ]; + } + } + + if (isPath) { + return path.join(","); + } else { + return paper.path(path); + } + } + +/*\ + * Paper.vbarchart + [ method ] + ** + * Creates a vertical bar chart + ** + > Parameters + ** + - x (number) x coordinate of the chart + - y (number) y coordinate of the chart + - width (number) width of the chart (respected by all elements in the set) + - height (number) height of the chart (respected by all elements in the set) + - values (array) values + - opts (object) options for the chart + o { + o type (string) type of endings of the bar. Default: 'square'. Other options are: 'round', 'sharp', 'soft'. + o gutter (number)(string) default '20%' (WHAT DOES IT DO?) + o vgutter (number) + o colors (array) colors be used repeatedly to plot the bars. If multicolumn bar is used each sequence of bars with use a different color. + o stacked (boolean) whether or not to tread values as in a stacked bar chart + o to + o stretch (boolean) + o } + ** + = (object) path element of the popup + > Usage + | r.vbarchart(0, 0, 620, 260, [76, 70, 67, 71, 69], {}) + \*/ + + function VBarchart(paper, x, y, width, height, values, opts) { + opts = opts || {}; + + var chartinst = this, + type = opts.type || "square", + gutter = parseFloat(opts.gutter || "20%"), + chart = paper.set(), + bars = paper.set(), + covers = paper.set(), + covers2 = paper.set(), + total = Math.max.apply(Math, values), + stacktotal = [], + multi = 0, + colors = opts.colors || chartinst.colors, + len = values.length; + + if (Raphael.is(values[0], "array")) { + total = []; + multi = len; + len = 0; + + for (var i = values.length; i--;) { + bars.push(paper.set()); + total.push(Math.max.apply(Math, values[i])); + len = Math.max(len, values[i].length); + } + + if (opts.stacked) { + for (var i = len; i--;) { + var tot = 0; + + for (var j = values.length; j--;) { + tot +=+ values[j][i] || 0; + } + + stacktotal.push(tot); + } + } + + for (var i = values.length; i--;) { + if (values[i].length < len) { + for (var j = len; j--;) { + values[i].push(0); + } + } + } + + total = Math.max.apply(Math, opts.stacked ? stacktotal : total); + } + + total = (opts.to) || total; + + var barwidth = width / (len * (100 + gutter) + gutter) * 100, + barhgutter = barwidth * gutter / 100, + barvgutter = opts.vgutter == null ? 20 : opts.vgutter, + stack = [], + X = x + barhgutter, + Y = (height - 2 * barvgutter) / total; + + if (!opts.stretch) { + barhgutter = Math.round(barhgutter); + barwidth = Math.floor(barwidth); + } + + !opts.stacked && (barwidth /= multi || 1); + + for (var i = 0; i < len; i++) { + stack = []; + + for (var j = 0; j < (multi || 1); j++) { + var h = Math.round((multi ? values[j][i] : values[i]) * Y), + top = y + height - barvgutter - h, + bar = finger(Math.round(X + barwidth / 2), top + h, barwidth, h, true, type, null, paper).attr({ stroke: "none", fill: colors[multi ? j : i] }); + + if (multi) { + bars[j].push(bar); + } else { + bars.push(bar); + } + + bar.y = top; + bar.x = Math.round(X + barwidth / 2); + bar.w = barwidth; + bar.h = h; + bar.value = multi ? values[j][i] : values[i]; + + if (!opts.stacked) { + X += barwidth; + } else { + stack.push(bar); + } + } + + if (opts.stacked) { + var cvr; + + covers2.push(cvr = paper.rect(stack[0].x - stack[0].w / 2, y, barwidth, height).attr(chartinst.shim)); + cvr.bars = paper.set(); + + var size = 0; + + for (var s = stack.length; s--;) { + stack[s].toFront(); + } + + for (var s = 0, ss = stack.length; s < ss; s++) { + var bar = stack[s], + cover, + h = (size + bar.value) * Y, + path = finger(bar.x, y + height - barvgutter - !!size * .5, barwidth, h, true, type, 1, paper); + + cvr.bars.push(bar); + size && bar.attr({path: path}); + bar.h = h; + bar.y = y + height - barvgutter - !!size * .5 - h; + covers.push(cover = paper.rect(bar.x - bar.w / 2, bar.y, barwidth, bar.value * Y).attr(chartinst.shim)); + cover.bar = bar; + cover.value = bar.value; + size += bar.value; + } + + X += barwidth; + } + + X += barhgutter; + } + + covers2.toFront(); + X = x + barhgutter; + + if (!opts.stacked) { + for (var i = 0; i < len; i++) { + for (var j = 0; j < (multi || 1); j++) { + var cover; + + covers.push(cover = paper.rect(Math.round(X), y + barvgutter, barwidth, height - barvgutter).attr(chartinst.shim)); + cover.bar = multi ? bars[j][i] : bars[i]; + cover.value = cover.bar.value; + X += barwidth; + } + + X += barhgutter; + } + } + + chart.label = function (labels, isBottom) { + labels = labels || []; + this.labels = paper.set(); + + var L, l = -Infinity; + + if (opts.stacked) { + for (var i = 0; i < len; i++) { + var tot = 0; + + for (var j = 0; j < (multi || 1); j++) { + tot += multi ? values[j][i] : values[i]; + + if (j == multi - 1) { + var label = paper.labelise(labels[i], tot, total); + + L = paper.text(bars[i * (multi || 1) + j].x, y + height - barvgutter / 2, label).attr(txtattr).insertBefore(covers[i * (multi || 1) + j]); + + var bb = L.getBBox(); + + if (bb.x - 7 < l) { + L.remove(); + } else { + this.labels.push(L); + l = bb.x + bb.width; + } + } + } + } + } else { + for (var i = 0; i < len; i++) { + for (var j = 0; j < (multi || 1); j++) { + var label = paper.labelise(multi ? labels[j] && labels[j][i] : labels[i], multi ? values[j][i] : values[i], total); + + L = paper.text(bars[i * (multi || 1) + j].x, isBottom ? y + height - barvgutter / 2 : bars[i * (multi || 1) + j].y - 10, label).attr(txtattr).insertBefore(covers[i * (multi || 1) + j]); + + var bb = L.getBBox(); + + if (bb.x - 7 < l) { + L.remove(); + } else { + this.labels.push(L); + l = bb.x + bb.width; + } + } + } + } + return this; + }; + + chart.hover = function (fin, fout) { + covers2.hide(); + covers.show(); + covers.mouseover(fin).mouseout(fout); + return this; + }; + + chart.hoverColumn = function (fin, fout) { + covers.hide(); + covers2.show(); + fout = fout || function () {}; + covers2.mouseover(fin).mouseout(fout); + return this; + }; + + chart.click = function (f) { + covers2.hide(); + covers.show(); + covers.click(f); + return this; + }; + + chart.each = function (f) { + if (!Raphael.is(f, "function")) { + return this; + } + for (var i = covers.length; i--;) { + f.call(covers[i]); + } + return this; + }; + + chart.eachColumn = function (f) { + if (!Raphael.is(f, "function")) { + return this; + } + for (var i = covers2.length; i--;) { + f.call(covers2[i]); + } + return this; + }; + + chart.clickColumn = function (f) { + covers.hide(); + covers2.show(); + covers2.click(f); + return this; + }; + + chart.push(bars, covers, covers2); + chart.bars = bars; + chart.covers = covers; + return chart; + }; + + //inheritance + var F = function() {}; + F.prototype = Raphael.g; + HBarchart.prototype = VBarchart.prototype = new F; //prototype reused by hbarchart + + Raphael.fn.barchart = function(x, y, width, height, values, opts) { + return new VBarchart(this, x, y, width, height, values, opts); + }; + +/*\ + * Paper.barchart + [ method ] + ** + * Creates a horizontal bar chart + ** + > Parameters + ** + - x (number) x coordinate of the chart + - y (number) y coordinate of the chart + - width (number) width of the chart (respected by all elements in the set) + - height (number) height of the chart (respected by all elements in the set) + - values (array) values + - opts (object) options for the chart + o { + o type (string) type of endings of the bar. Default: 'square'. Other options are: 'round', 'sharp', 'soft'. + o gutter (number)(string) default '20%' (WHAT DOES IT DO?) + o vgutter (number) + o colors (array) colors be used repeatedly to plot the bars. If multicolumn bar is used each sequence of bars with use a different color. + o stacked (boolean) whether or not to tread values as in a stacked bar chart + o to + o stretch (boolean) + o } + ** + = (object) path element of the popup + > Usage + | r.barchart(0, 0, 620, 260, [76, 70, 67, 71, 69], {}) + \*/ + + function HBarchart(paper, x, y, width, height, values, opts) { + opts = opts || {}; + + var chartinst = this, + type = opts.type || "square", + gutter = parseFloat(opts.gutter || "20%"), + chart = paper.set(), + bars = paper.set(), + covers = paper.set(), + covers2 = paper.set(), + total = Math.max.apply(Math, values), + stacktotal = [], + multi = 0, + colors = opts.colors || chartinst.colors, + len = values.length; + + if (Raphael.is(values[0], "array")) { + total = []; + multi = len; + len = 0; + + for (var i = values.length; i--;) { + bars.push(paper.set()); + total.push(Math.max.apply(Math, values[i])); + len = Math.max(len, values[i].length); + } + + if (opts.stacked) { + for (var i = len; i--;) { + var tot = 0; + for (var j = values.length; j--;) { + tot +=+ values[j][i] || 0; + } + stacktotal.push(tot); + } + } + + for (var i = values.length; i--;) { + if (values[i].length < len) { + for (var j = len; j--;) { + values[i].push(0); + } + } + } + + total = Math.max.apply(Math, opts.stacked ? stacktotal : total); + } + + total = (opts.to) || total; + + var barheight = Math.floor(height / (len * (100 + gutter) + gutter) * 100), + bargutter = Math.floor(barheight * gutter / 100), + stack = [], + Y = y + bargutter, + X = (width - 1) / total; + + !opts.stacked && (barheight /= multi || 1); + + for (var i = 0; i < len; i++) { + stack = []; + + for (var j = 0; j < (multi || 1); j++) { + var val = multi ? values[j][i] : values[i], + bar = finger(x, Y + barheight / 2, Math.round(val * X), barheight - 1, false, type, null, paper).attr({stroke: "none", fill: colors[multi ? j : i]}); + + if (multi) { + bars[j].push(bar); + } else { + bars.push(bar); + } + + bar.x = x + Math.round(val * X); + bar.y = Y + barheight / 2; + bar.w = Math.round(val * X); + bar.h = barheight; + bar.value = +val; + + if (!opts.stacked) { + Y += barheight; + } else { + stack.push(bar); + } + } + + if (opts.stacked) { + var cvr = paper.rect(x, stack[0].y - stack[0].h / 2, width, barheight).attr(chartinst.shim); + + covers2.push(cvr); + cvr.bars = paper.set(); + + var size = 0; + + for (var s = stack.length; s--;) { + stack[s].toFront(); + } + + for (var s = 0, ss = stack.length; s < ss; s++) { + var bar = stack[s], + cover, + val = Math.round((size + bar.value) * X), + path = finger(x, bar.y, val, barheight - 1, false, type, 1, paper); + + cvr.bars.push(bar); + size && bar.attr({ path: path }); + bar.w = val; + bar.x = x + val; + covers.push(cover = paper.rect(x + size * X, bar.y - bar.h / 2, bar.value * X, barheight).attr(chartinst.shim)); + cover.bar = bar; + size += bar.value; + } + + Y += barheight; + } + + Y += bargutter; + } + + covers2.toFront(); + Y = y + bargutter; + + if (!opts.stacked) { + for (var i = 0; i < len; i++) { + for (var j = 0; j < (multi || 1); j++) { + var cover = paper.rect(x, Y, width, barheight).attr(chartinst.shim); + + covers.push(cover); + cover.bar = multi ? bars[j][i] : bars[i]; + cover.value = cover.bar.value; + Y += barheight; + } + + Y += bargutter; + } + } + + chart.label = function (labels, isRight) { + labels = labels || []; + this.labels = paper.set(); + + for (var i = 0; i < len; i++) { + for (var j = 0; j < multi; j++) { + var label = paper.labelise(multi ? labels[j] && labels[j][i] : labels[i], multi ? values[j][i] : values[i], total), + X = isRight ? bars[i * (multi || 1) + j].x - barheight / 2 + 3 : x + 5, + A = isRight ? "end" : "start", + L; + + this.labels.push(L = paper.text(X, bars[i * (multi || 1) + j].y, label).attr(txtattr).attr({ "text-anchor": A }).insertBefore(covers[0])); + + if (L.getBBox().x < x + 5) { + L.attr({x: x + 5, "text-anchor": "start"}); + } else { + bars[i * (multi || 1) + j].label = L; + } + } + } + + return this; + }; + + chart.hover = function (fin, fout) { + covers2.hide(); + covers.show(); + fout = fout || function () {}; + covers.mouseover(fin).mouseout(fout); + return this; + }; + + chart.hoverColumn = function (fin, fout) { + covers.hide(); + covers2.show(); + fout = fout || function () {}; + covers2.mouseover(fin).mouseout(fout); + return this; + }; + + chart.each = function (f) { + if (!Raphael.is(f, "function")) { + return this; + } + for (var i = covers.length; i--;) { + f.call(covers[i]); + } + return this; + }; + + chart.eachColumn = function (f) { + if (!Raphael.is(f, "function")) { + return this; + } + for (var i = covers2.length; i--;) { + f.call(covers2[i]); + } + return this; + }; + + chart.click = function (f) { + covers2.hide(); + covers.show(); + covers.click(f); + return this; + }; + + chart.clickColumn = function (f) { + covers.hide(); + covers2.show(); + covers2.click(f); + return this; + }; + + chart.push(bars, covers, covers2); + chart.bars = bars; + chart.covers = covers; + return chart; + }; + + Raphael.fn.hbarchart = function(x, y, width, height, values, opts) { + return new HBarchart(this, x, y, width, height, values, opts); + }; + +})(); -- cgit v1.2.1 From e202564a3a92beeb49afc4d1ae756b229a25b5e4 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 19 Jan 2016 20:50:33 -0200 Subject: Don't vendor minified jQuery.nicescroll --- app/assets/javascripts/application.js.coffee | 2 +- vendor/assets/javascripts/jquery.nicescroll.js | 3634 ++++++++++++++++++++ vendor/assets/javascripts/jquery.nicescroll.min.js | 118 - 3 files changed, 3635 insertions(+), 119 deletions(-) create mode 100644 vendor/assets/javascripts/jquery.nicescroll.js delete mode 100644 vendor/assets/javascripts/jquery.nicescroll.min.js diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 981400a7c31..9f9b94d1a4a 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -38,7 +38,7 @@ #= require shortcuts_dashboard_navigation #= require shortcuts_issuable #= require shortcuts_network -#= require jquery.nicescroll.min +#= require jquery.nicescroll #= require_tree . #= require fuzzaldrin-plus.min diff --git a/vendor/assets/javascripts/jquery.nicescroll.js b/vendor/assets/javascripts/jquery.nicescroll.js new file mode 100644 index 00000000000..7653f25df4b --- /dev/null +++ b/vendor/assets/javascripts/jquery.nicescroll.js @@ -0,0 +1,3634 @@ +/* jquery.nicescroll +-- version 3.6.0 +-- copyright 2014-11-21 InuYaksa*2014 +-- licensed under the MIT +-- +-- http://nicescroll.areaaperta.com/ +-- https://github.com/inuyaksa/jquery.nicescroll +-- +*/ + +(function(factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as anonymous module. + define(['jquery'], factory); + } else { + // Browser globals. + factory(jQuery); + } +}(function(jQuery) { + "use strict"; + + // globals + var domfocus = false; + var mousefocus = false; + var tabindexcounter = 0; + var ascrailcounter = 2000; + var globalmaxzindex = 0; + + var $ = jQuery; // sandbox + + // http://stackoverflow.com/questions/2161159/get-script-path + function getScriptPath() { + var scripts = document.getElementsByTagName('script'); + var path = scripts[scripts.length - 1].src.split('?')[0]; + return (path.split('/').length > 0) ? path.split('/').slice(0, -1).join('/') + '/' : ''; + } + + var vendors = ['webkit','ms','moz','o']; + + var setAnimationFrame = window.requestAnimationFrame || false; + var clearAnimationFrame = window.cancelAnimationFrame || false; + + if (!setAnimationFrame) { // legacy detection + for (var vx in vendors) { + var v = vendors[vx]; + if (!setAnimationFrame) setAnimationFrame = window[v + 'RequestAnimationFrame']; + if (!clearAnimationFrame) clearAnimationFrame = window[v + 'CancelAnimationFrame'] || window[v + 'CancelRequestAnimationFrame']; + } + } + + var ClsMutationObserver = window.MutationObserver || window.WebKitMutationObserver || false; + + var _globaloptions = { + zindex: "auto", + cursoropacitymin: 0, + cursoropacitymax: 1, + cursorcolor: "#424242", + cursorwidth: "5px", + cursorborder: "1px solid #fff", + cursorborderradius: "5px", + scrollspeed: 60, + mousescrollstep: 8 * 3, + touchbehavior: false, + hwacceleration: true, + usetransition: true, + boxzoom: false, + dblclickzoom: true, + gesturezoom: true, + grabcursorenabled: true, + autohidemode: true, + background: "", + iframeautoresize: true, + cursorminheight: 32, + preservenativescrolling: true, + railoffset: false, + railhoffset: false, + bouncescroll: true, + spacebarenabled: true, + railpadding: { + top: 0, + right: 0, + left: 0, + bottom: 0 + }, + disableoutline: true, + horizrailenabled: true, + railalign: "right", + railvalign: "bottom", + enabletranslate3d: true, + enablemousewheel: true, + enablekeyboard: true, + smoothscroll: true, + sensitiverail: true, + enablemouselockapi: true, + // cursormaxheight:false, + cursorfixedheight: false, + directionlockdeadzone: 6, + hidecursordelay: 400, + nativeparentscrolling: true, + enablescrollonselection: true, + overflowx: true, + overflowy: true, + cursordragspeed: 0.3, + rtlmode: "auto", + cursordragontouch: false, + oneaxismousemode: "auto", + scriptpath: getScriptPath(), + preventmultitouchscrolling: true + }; + + var browserdetected = false; + + var getBrowserDetection = function() { + + if (browserdetected) return browserdetected; + + var _el = document.createElement('DIV'), + _style = _el.style, + _agent = navigator.userAgent, + _platform = navigator.platform, + d = {}; + + d.haspointerlock = "pointerLockElement" in document || "webkitPointerLockElement" in document || "mozPointerLockElement" in document; + + d.isopera = ("opera" in window); // 12- + d.isopera12 = (d.isopera && ("getUserMedia" in navigator)); + d.isoperamini = (Object.prototype.toString.call(window.operamini) === "[object OperaMini]"); + + d.isie = (("all" in document) && ("attachEvent" in _el) && !d.isopera); //IE10- + d.isieold = (d.isie && !("msInterpolationMode" in _style)); // IE6 and older + d.isie7 = d.isie && !d.isieold && (!("documentMode" in document) || (document.documentMode == 7)); + d.isie8 = d.isie && ("documentMode" in document) && (document.documentMode == 8); + d.isie9 = d.isie && ("performance" in window) && (document.documentMode >= 9); + d.isie10 = d.isie && ("performance" in window) && (document.documentMode == 10); + d.isie11 = ("msRequestFullscreen" in _el) && (document.documentMode >= 11); // IE11+ + + d.isie9mobile = /iemobile.9/i.test(_agent); //wp 7.1 mango + if (d.isie9mobile) d.isie9 = false; + d.isie7mobile = (!d.isie9mobile && d.isie7) && /iemobile/i.test(_agent); //wp 7.0 + + d.ismozilla = ("MozAppearance" in _style); + + d.iswebkit = ("WebkitAppearance" in _style); + + d.ischrome = ("chrome" in window); + d.ischrome22 = (d.ischrome && d.haspointerlock); + d.ischrome26 = (d.ischrome && ("transition" in _style)); // issue with transform detection (maintain prefix) + + d.cantouch = ("ontouchstart" in document.documentElement) || ("ontouchstart" in window); // detection for Chrome Touch Emulation + d.hasmstouch = (window.MSPointerEvent || false); // IE10 pointer events + d.hasw3ctouch = (window.PointerEvent || false); //IE11 pointer events, following W3C Pointer Events spec + + d.ismac = /^mac$/i.test(_platform); + + d.isios = (d.cantouch && /iphone|ipad|ipod/i.test(_platform)); + d.isios4 = ((d.isios) && !("seal" in Object)); + d.isios7 = ((d.isios)&&("webkitHidden" in document)); //iOS 7+ + + d.isandroid = (/android/i.test(_agent)); + + d.haseventlistener = ("addEventListener" in _el); + + d.trstyle = false; + d.hastransform = false; + d.hastranslate3d = false; + d.transitionstyle = false; + d.hastransition = false; + d.transitionend = false; + + var a; + var check = ['transform', 'msTransform', 'webkitTransform', 'MozTransform', 'OTransform']; + for (a = 0; a < check.length; a++) { + if (typeof _style[check[a]] != "undefined") { + d.trstyle = check[a]; + break; + } + } + d.hastransform = (!!d.trstyle); + if (d.hastransform) { + _style[d.trstyle] = "translate3d(1px,2px,3px)"; + d.hastranslate3d = /translate3d/.test(_style[d.trstyle]); + } + + d.transitionstyle = false; + d.prefixstyle = ''; + d.transitionend = false; + check = ['transition', 'webkitTransition', 'msTransition', 'MozTransition', 'OTransition', 'OTransition', 'KhtmlTransition']; + var prefix = ['', '-webkit-', '-ms-', '-moz-', '-o-', '-o', '-khtml-']; + var evs = ['transitionend', 'webkitTransitionEnd', 'msTransitionEnd', 'transitionend', 'otransitionend', 'oTransitionEnd', 'KhtmlTransitionEnd']; + for (a = 0; a < check.length; a++) { + if (check[a] in _style) { + d.transitionstyle = check[a]; + d.prefixstyle = prefix[a]; + d.transitionend = evs[a]; + break; + } + } + if (d.ischrome26) { // always use prefix + d.prefixstyle = prefix[1]; + } + + d.hastransition = (d.transitionstyle); + + function detectCursorGrab() { + var lst = ['-webkit-grab', '-moz-grab', 'grab']; + if ((d.ischrome && !d.ischrome22) || d.isie) lst = []; // force setting for IE returns false positive and chrome cursor bug + for (var a = 0; a < lst.length; a++) { + var p = lst[a]; + _style.cursor = p; + if (_style.cursor == p) return p; + } + return 'url(//mail.google.com/mail/images/2/openhand.cur),n-resize'; // thank you google for custom cursor! + } + d.cursorgrabvalue = detectCursorGrab(); + + d.hasmousecapture = ("setCapture" in _el); + + d.hasMutationObserver = (ClsMutationObserver !== false); + + _el = null; //memory released + + browserdetected = d; + + return d; + }; + + var NiceScrollClass = function(myopt, me) { + + var self = this; + + this.version = '3.6.0'; + this.name = 'nicescroll'; + + this.me = me; + + this.opt = { + doc: $("body"), + win: false + }; + + $.extend(this.opt, _globaloptions); // clone opts + + // Options for internal use + this.opt.snapbackspeed = 80; + + if (myopt || false) { + for (var a in self.opt) { + if (typeof myopt[a] != "undefined") self.opt[a] = myopt[a]; + } + } + + this.doc = self.opt.doc; + this.iddoc = (this.doc && this.doc[0]) ? this.doc[0].id || '' : ''; + this.ispage = /^BODY|HTML/.test((self.opt.win) ? self.opt.win[0].nodeName : this.doc[0].nodeName); + this.haswrapper = (self.opt.win !== false); + this.win = self.opt.win || (this.ispage ? $(window) : this.doc); + this.docscroll = (this.ispage && !this.haswrapper) ? $(window) : this.win; + this.body = $("body"); + this.viewport = false; + + this.isfixed = false; + + this.iframe = false; + this.isiframe = ((this.doc[0].nodeName == 'IFRAME') && (this.win[0].nodeName == 'IFRAME')); + + this.istextarea = (this.win[0].nodeName == 'TEXTAREA'); + + this.forcescreen = false; //force to use screen position on events + + this.canshowonmouseevent = (self.opt.autohidemode != "scroll"); + + // Events jump table + this.onmousedown = false; + this.onmouseup = false; + this.onmousemove = false; + this.onmousewheel = false; + this.onkeypress = false; + this.ongesturezoom = false; + this.onclick = false; + + // Nicescroll custom events + this.onscrollstart = false; + this.onscrollend = false; + this.onscrollcancel = false; + + this.onzoomin = false; + this.onzoomout = false; + + // Let's start! + this.view = false; + this.page = false; + + this.scroll = { + x: 0, + y: 0 + }; + this.scrollratio = { + x: 0, + y: 0 + }; + this.cursorheight = 20; + this.scrollvaluemax = 0; + + this.isrtlmode = (this.opt.rtlmode == "auto") ? ((this.win[0] == window ? this.body : this.win).css("direction") == "rtl") : (this.opt.rtlmode === true); + // this.checkrtlmode = false; + + this.scrollrunning = false; + + this.scrollmom = false; + + this.observer = false; // observer div changes + this.observerremover = false; // observer on parent for remove detection + this.observerbody = false; // observer on body for position change + + do { + this.id = "ascrail" + (ascrailcounter++); + } while (document.getElementById(this.id)); + + this.rail = false; + this.cursor = false; + this.cursorfreezed = false; + this.selectiondrag = false; + + this.zoom = false; + this.zoomactive = false; + + this.hasfocus = false; + this.hasmousefocus = false; + + this.visibility = true; + this.railslocked = false; // locked by resize + this.locked = false; // prevent lost of locked status sets by user + this.hidden = false; // rails always hidden + this.cursoractive = true; // user can interact with cursors + + this.wheelprevented = false; //prevent mousewheel event + + this.overflowx = self.opt.overflowx; + this.overflowy = self.opt.overflowy; + + this.nativescrollingarea = false; + this.checkarea = 0; + + this.events = []; // event list for unbind + + this.saved = {}; // style saved + + this.delaylist = {}; + this.synclist = {}; + + this.lastdeltax = 0; + this.lastdeltay = 0; + + this.detected = getBrowserDetection(); + + var cap = $.extend({}, this.detected); + + this.canhwscroll = (cap.hastransform && self.opt.hwacceleration); + this.ishwscroll = (this.canhwscroll && self.haswrapper); + + this.hasreversehr = (this.isrtlmode&&!cap.iswebkit); //RTL mode with reverse horizontal axis + + this.istouchcapable = false; // desktop devices with touch screen support + + //## Check WebKit-based desktop with touch support + //## + Firefox 18 nightly build (desktop) false positive (or desktop with touch support) + if (cap.cantouch && !cap.isios && !cap.isandroid && (cap.iswebkit || cap.ismozilla)) { + this.istouchcapable = true; + cap.cantouch = false; // parse normal desktop events + } + + //## disable MouseLock API on user request + if (!self.opt.enablemouselockapi) { + cap.hasmousecapture = false; + cap.haspointerlock = false; + } + +/* deprecated + this.delayed = function(name, fn, tm, lazy) { + }; +*/ + + this.debounced = function(name, fn, tm) { + var dd = self.delaylist[name]; + self.delaylist[name] = fn; + if (!dd) { + setTimeout(function() { + var fn = self.delaylist[name]; + self.delaylist[name] = false; + fn.call(self); + }, tm); + } + }; + + var _onsync = false; + + this.synched = function(name, fn) { + + function requestSync() { + if (_onsync) return; + setAnimationFrame(function() { + _onsync = false; + for (var nn in self.synclist) { + var fn = self.synclist[nn]; + if (fn) fn.call(self); + self.synclist[nn] = false; + } + }); + _onsync = true; + } + + self.synclist[name] = fn; + requestSync(); + return name; + }; + + this.unsynched = function(name) { + if (self.synclist[name]) self.synclist[name] = false; + }; + + this.css = function(el, pars) { // save & set + for (var n in pars) { + self.saved.css.push([el, n, el.css(n)]); + el.css(n, pars[n]); + } + }; + + this.scrollTop = function(val) { + return (typeof val == "undefined") ? self.getScrollTop() : self.setScrollTop(val); + }; + + this.scrollLeft = function(val) { + return (typeof val == "undefined") ? self.getScrollLeft() : self.setScrollLeft(val); + }; + + // derived by by Dan Pupius www.pupius.net + var BezierClass = function(st, ed, spd, p1, p2, p3, p4) { + + this.st = st; + this.ed = ed; + this.spd = spd; + + this.p1 = p1 || 0; + this.p2 = p2 || 1; + this.p3 = p3 || 0; + this.p4 = p4 || 1; + + this.ts = (new Date()).getTime(); + this.df = this.ed - this.st; + }; + BezierClass.prototype = { + B2: function(t) { + return 3 * t * t * (1 - t); + }, + B3: function(t) { + return 3 * t * (1 - t) * (1 - t); + }, + B4: function(t) { + return (1 - t) * (1 - t) * (1 - t); + }, + getNow: function() { + var nw = (new Date()).getTime(); + var pc = 1 - ((nw - this.ts) / this.spd); + var bz = this.B2(pc) + this.B3(pc) + this.B4(pc); + return (pc < 0) ? this.ed : this.st + Math.round(this.df * bz); + }, + update: function(ed, spd) { + this.st = this.getNow(); + this.ed = ed; + this.spd = spd; + this.ts = (new Date()).getTime(); + this.df = this.ed - this.st; + return this; + } + }; + + //derived from http://stackoverflow.com/questions/11236090/ + function getMatrixValues() { + var tr = self.doc.css(cap.trstyle); + if (tr && (tr.substr(0, 6) == "matrix")) { + return tr.replace(/^.*\((.*)\)$/g, "$1").replace(/px/g, '').split(/, +/); + } + return false; + } + + if (this.ishwscroll) { + // hw accelerated scroll + this.doc.translate = { + x: 0, + y: 0, + tx: "0px", + ty: "0px" + }; + + //this one can help to enable hw accel on ios6 http://indiegamr.com/ios6-html-hardware-acceleration-changes-and-how-to-fix-them/ + if (cap.hastranslate3d && cap.isios) this.doc.css("-webkit-backface-visibility", "hidden"); // prevent flickering http://stackoverflow.com/questions/3461441/ + + this.getScrollTop = function(last) { + if (!last) { + var mtx = getMatrixValues(); + if (mtx) return (mtx.length == 16) ? -mtx[13] : -mtx[5]; //matrix3d 16 on IE10 + if (self.timerscroll && self.timerscroll.bz) return self.timerscroll.bz.getNow(); + } + return self.doc.translate.y; + }; + + this.getScrollLeft = function(last) { + if (!last) { + var mtx = getMatrixValues(); + if (mtx) return (mtx.length == 16) ? -mtx[12] : -mtx[4]; //matrix3d 16 on IE10 + if (self.timerscroll && self.timerscroll.bh) return self.timerscroll.bh.getNow(); + } + return self.doc.translate.x; + }; + + this.notifyScrollEvent = function(el) { + var e = document.createEvent("UIEvents"); + e.initUIEvent("scroll", false, true, window, 1); + e.niceevent = true; + el.dispatchEvent(e); + }; + + var cxscrollleft = (this.isrtlmode) ? 1 : -1; + + if (cap.hastranslate3d && self.opt.enabletranslate3d) { + this.setScrollTop = function(val, silent) { + self.doc.translate.y = val; + self.doc.translate.ty = (val * -1) + "px"; + self.doc.css(cap.trstyle, "translate3d(" + self.doc.translate.tx + "," + self.doc.translate.ty + ",0px)"); + if (!silent) self.notifyScrollEvent(self.win[0]); + }; + this.setScrollLeft = function(val, silent) { + self.doc.translate.x = val; + self.doc.translate.tx = (val * cxscrollleft) + "px"; + self.doc.css(cap.trstyle, "translate3d(" + self.doc.translate.tx + "," + self.doc.translate.ty + ",0px)"); + if (!silent) self.notifyScrollEvent(self.win[0]); + }; + } else { + this.setScrollTop = function(val, silent) { + self.doc.translate.y = val; + self.doc.translate.ty = (val * -1) + "px"; + self.doc.css(cap.trstyle, "translate(" + self.doc.translate.tx + "," + self.doc.translate.ty + ")"); + if (!silent) self.notifyScrollEvent(self.win[0]); + }; + this.setScrollLeft = function(val, silent) { + self.doc.translate.x = val; + self.doc.translate.tx = (val * cxscrollleft) + "px"; + self.doc.css(cap.trstyle, "translate(" + self.doc.translate.tx + "," + self.doc.translate.ty + ")"); + if (!silent) self.notifyScrollEvent(self.win[0]); + }; + } + } else { + // native scroll + this.getScrollTop = function() { + return self.docscroll.scrollTop(); + }; + this.setScrollTop = function(val) { + return self.docscroll.scrollTop(val); + }; + this.getScrollLeft = function() { + if (self.detected.ismozilla && self.isrtlmode) + return Math.abs(self.docscroll.scrollLeft()); + return self.docscroll.scrollLeft(); + }; + this.setScrollLeft = function(val) { + return self.docscroll.scrollLeft((self.detected.ismozilla && self.isrtlmode) ? -val : val); + }; + } + + this.getTarget = function(e) { + if (!e) return false; + if (e.target) return e.target; + if (e.srcElement) return e.srcElement; + return false; + }; + + this.hasParent = function(e, id) { + if (!e) return false; + var el = e.target || e.srcElement || e || false; + while (el && el.id != id) { + el = el.parentNode || false; + } + return (el !== false); + }; + + function getZIndex() { + var dom = self.win; + if ("zIndex" in dom) return dom.zIndex(); // use jQuery UI method when available + while (dom.length > 0) { + if (dom[0].nodeType == 9) return false; + var zi = dom.css('zIndex'); + if (!isNaN(zi) && zi != 0) return parseInt(zi); + dom = dom.parent(); + } + return false; + } + + //inspired by http://forum.jquery.com/topic/width-includes-border-width-when-set-to-thin-medium-thick-in-ie + var _convertBorderWidth = { + "thin": 1, + "medium": 3, + "thick": 5 + }; + + function getWidthToPixel(dom, prop, chkheight) { + var wd = dom.css(prop); + var px = parseFloat(wd); + if (isNaN(px)) { + px = _convertBorderWidth[wd] || 0; + var brd = (px == 3) ? ((chkheight) ? (self.win.outerHeight() - self.win.innerHeight()) : (self.win.outerWidth() - self.win.innerWidth())) : 1; //DON'T TRUST CSS + if (self.isie8 && px) px += 1; + return (brd) ? px : 0; + } + return px; + } + + this.getDocumentScrollOffset = function() { + return {top:window.pageYOffset||document.documentElement.scrollTop, + left:window.pageXOffset||document.documentElement.scrollLeft}; + } + + this.getOffset = function() { + if (self.isfixed) { + var ofs = self.win.offset(); // fix Chrome auto issue (when right/bottom props only) + var scrl = self.getDocumentScrollOffset(); + ofs.top-=scrl.top; + ofs.left-=scrl.left; + return ofs; + } + var ww = self.win.offset(); + if (!self.viewport) return ww; + var vp = self.viewport.offset(); + return { + top: ww.top - vp.top,// + self.viewport.scrollTop(), + left: ww.left - vp.left // + self.viewport.scrollLeft() + }; + }; + + this.updateScrollBar = function(len) { + if (self.ishwscroll) { + self.rail.css({ //** + height: self.win.innerHeight() - (self.opt.railpadding.top + self.opt.railpadding.bottom) + }); + if (self.railh) self.railh.css({ //** + width: self.win.innerWidth() - (self.opt.railpadding.left + self.opt.railpadding.right) + }); + + } else { + var wpos = self.getOffset(); + var pos = { + top: wpos.top, + left: wpos.left - (self.opt.railpadding.left + self.opt.railpadding.right) + }; + pos.top += getWidthToPixel(self.win, 'border-top-width', true); + pos.left += (self.rail.align) ? self.win.outerWidth() - getWidthToPixel(self.win, 'border-right-width') - self.rail.width : getWidthToPixel(self.win, 'border-left-width'); + + var off = self.opt.railoffset; + if (off) { + if (off.top) pos.top += off.top; + if (self.rail.align && off.left) pos.left += off.left; + } + + if (!self.railslocked) self.rail.css({ + top: pos.top, + left: pos.left, + height: ((len) ? len.h : self.win.innerHeight()) - (self.opt.railpadding.top + self.opt.railpadding.bottom) + }); + + if (self.zoom) { + self.zoom.css({ + top: pos.top + 1, + left: (self.rail.align == 1) ? pos.left - 20 : pos.left + self.rail.width + 4 + }); + } + + if (self.railh && !self.railslocked) { + var pos = { + top: wpos.top, + left: wpos.left + }; + var off = self.opt.railhoffset; + if (!!off) { + if (!!off.top) pos.top += off.top; + if (!!off.left) pos.left += off.left; + } + var y = (self.railh.align) ? pos.top + getWidthToPixel(self.win, 'border-top-width', true) + self.win.innerHeight() - self.railh.height : pos.top + getWidthToPixel(self.win, 'border-top-width', true); + var x = pos.left + getWidthToPixel(self.win, 'border-left-width'); + self.railh.css({ + top: y - (self.opt.railpadding.top + self.opt.railpadding.bottom), + left: x, + width: self.railh.width + }); + } + + + } + }; + + this.doRailClick = function(e, dbl, hr) { + var fn, pg, cur, pos; + + if (self.railslocked) return; + self.cancelEvent(e); + + if (dbl) { + fn = (hr) ? self.doScrollLeft : self.doScrollTop; + cur = (hr) ? ((e.pageX - self.railh.offset().left - (self.cursorwidth / 2)) * self.scrollratio.x) : ((e.pageY - self.rail.offset().top - (self.cursorheight / 2)) * self.scrollratio.y); + fn(cur); + } else { + fn = (hr) ? self.doScrollLeftBy : self.doScrollBy; + cur = (hr) ? self.scroll.x : self.scroll.y; + pos = (hr) ? e.pageX - self.railh.offset().left : e.pageY - self.rail.offset().top; + pg = (hr) ? self.view.w : self.view.h; + fn((cur >= pos) ? pg: -pg);// (cur >= pos) ? fn(pg): fn(-pg); + } + + }; + + self.hasanimationframe = (setAnimationFrame); + self.hascancelanimationframe = (clearAnimationFrame); + + if (!self.hasanimationframe) { + setAnimationFrame = function(fn) { + return setTimeout(fn, 15 - Math.floor((+new Date()) / 1000) % 16); + }; // 1000/60)}; + clearAnimationFrame = clearInterval; + } else if (!self.hascancelanimationframe) clearAnimationFrame = function() { + self.cancelAnimationFrame = true; + }; + + this.init = function() { + + self.saved.css = []; + + if (cap.isie7mobile) return true; // SORRY, DO NOT WORK! + if (cap.isoperamini) return true; // SORRY, DO NOT WORK! + + if (cap.hasmstouch) self.css((self.ispage) ? $("html") : self.win, { + '-ms-touch-action': 'none' + }); + + self.zindex = "auto"; + if (!self.ispage && self.opt.zindex == "auto") { + self.zindex = getZIndex() || "auto"; + } else { + self.zindex = self.opt.zindex; + } + + if (!self.ispage && self.zindex != "auto") { + if (self.zindex > globalmaxzindex) globalmaxzindex = self.zindex; + } + + if (self.isie && self.zindex == 0 && self.opt.zindex == "auto") { // fix IE auto == 0 + self.zindex = "auto"; + } + + if (!self.ispage || (!cap.cantouch && !cap.isieold && !cap.isie9mobile)) { + + var cont = self.docscroll; + if (self.ispage) cont = (self.haswrapper) ? self.win : self.doc; + + if (!cap.isie9mobile) self.css(cont, { + 'overflow-y': 'hidden' + }); + + if (self.ispage && cap.isie7) { + if (self.doc[0].nodeName == 'BODY') self.css($("html"), { + 'overflow-y': 'hidden' + }); //IE7 double scrollbar issue + else if (self.doc[0].nodeName == 'HTML') self.css($("body"), { + 'overflow-y': 'hidden' + }); //IE7 double scrollbar issue + } + + if (cap.isios && !self.ispage && !self.haswrapper) self.css($("body"), { + "-webkit-overflow-scrolling": "touch" + }); //force hw acceleration + + var cursor = $(document.createElement('div')); + cursor.css({ + position: "relative", + top: 0, + "float": "right", + width: self.opt.cursorwidth, + height: "0px", + 'background-color': self.opt.cursorcolor, + border: self.opt.cursorborder, + 'background-clip': 'padding-box', + '-webkit-border-radius': self.opt.cursorborderradius, + '-moz-border-radius': self.opt.cursorborderradius, + 'border-radius': self.opt.cursorborderradius + }); + + cursor.hborder = parseFloat(cursor.outerHeight() - cursor.innerHeight()); + + cursor.addClass('nicescroll-cursors'); + + self.cursor = cursor; + + var rail = $(document.createElement('div')); + rail.attr('id', self.id); + rail.addClass('nicescroll-rails nicescroll-rails-vr'); + + var v, a, kp = ["left","right","top","bottom"]; //** + for (var n in kp) { + a = kp[n]; + v = self.opt.railpadding[a]; + (v) ? rail.css("padding-"+a,v+"px") : self.opt.railpadding[a] = 0; + } + + rail.append(cursor); + + rail.width = Math.max(parseFloat(self.opt.cursorwidth), cursor.outerWidth()); + rail.css({ + width: rail.width + "px", + 'zIndex': self.zindex, + "background": self.opt.background, + cursor: "default" + }); + + rail.visibility = true; + rail.scrollable = true; + + rail.align = (self.opt.railalign == "left") ? 0 : 1; + + self.rail = rail; + + self.rail.drag = false; + + var zoom = false; + if (self.opt.boxzoom && !self.ispage && !cap.isieold) { + zoom = document.createElement('div'); + + self.bind(zoom, "click", self.doZoom); + self.bind(zoom, "mouseenter", function() { + self.zoom.css('opacity', self.opt.cursoropacitymax); + }); + self.bind(zoom, "mouseleave", function() { + self.zoom.css('opacity', self.opt.cursoropacitymin); + }); + + self.zoom = $(zoom); + self.zoom.css({ + "cursor": "pointer", + 'z-index': self.zindex, + 'backgroundImage': 'url(' + self.opt.scriptpath + 'zoomico.png)', + 'height': 18, + 'width': 18, + 'backgroundPosition': '0px 0px' + }); + if (self.opt.dblclickzoom) self.bind(self.win, "dblclick", self.doZoom); + if (cap.cantouch && self.opt.gesturezoom) { + self.ongesturezoom = function(e) { + if (e.scale > 1.5) self.doZoomIn(e); + if (e.scale < 0.8) self.doZoomOut(e); + return self.cancelEvent(e); + }; + self.bind(self.win, "gestureend", self.ongesturezoom); + } + } + + // init HORIZ + + self.railh = false; + var railh; + + if (self.opt.horizrailenabled) { + + self.css(cont, { + 'overflow-x': 'hidden' + }); + + var cursor = $(document.createElement('div')); + cursor.css({ + position: "absolute", + top: 0, + height: self.opt.cursorwidth, + width: "0px", + 'background-color': self.opt.cursorcolor, + border: self.opt.cursorborder, + 'background-clip': 'padding-box', + '-webkit-border-radius': self.opt.cursorborderradius, + '-moz-border-radius': self.opt.cursorborderradius, + 'border-radius': self.opt.cursorborderradius + }); + + if (cap.isieold) cursor.css({'overflow':'hidden'}); //IE6 horiz scrollbar issue + + cursor.wborder = parseFloat(cursor.outerWidth() - cursor.innerWidth()); + + cursor.addClass('nicescroll-cursors'); + + self.cursorh = cursor; + + railh = $(document.createElement('div')); + railh.attr('id', self.id + '-hr'); + railh.addClass('nicescroll-rails nicescroll-rails-hr'); + railh.height = Math.max(parseFloat(self.opt.cursorwidth), cursor.outerHeight()); + railh.css({ + height: railh.height + "px", + 'zIndex': self.zindex, + "background": self.opt.background + }); + + railh.append(cursor); + + railh.visibility = true; + railh.scrollable = true; + + railh.align = (self.opt.railvalign == "top") ? 0 : 1; + + self.railh = railh; + + self.railh.drag = false; + + } + + // + + if (self.ispage) { + rail.css({ + position: "fixed", + top: "0px", + height: "100%" + }); + (rail.align) ? rail.css({ + right: "0px" + }): rail.css({ + left: "0px" + }); + self.body.append(rail); + if (self.railh) { + railh.css({ + position: "fixed", + left: "0px", + width: "100%" + }); + (railh.align) ? railh.css({ + bottom: "0px" + }): railh.css({ + top: "0px" + }); + self.body.append(railh); + } + } else { + if (self.ishwscroll) { + if (self.win.css('position') == 'static') self.css(self.win, { + 'position': 'relative' + }); + var bd = (self.win[0].nodeName == 'HTML') ? self.body : self.win; + $(bd).scrollTop(0).scrollLeft(0); // fix rail position if content already scrolled + if (self.zoom) { + self.zoom.css({ + position: "absolute", + top: 1, + right: 0, + "margin-right": rail.width + 4 + }); + bd.append(self.zoom); + } + rail.css({ + position: "absolute", + top: 0 + }); + (rail.align) ? rail.css({ + right: 0 + }): rail.css({ + left: 0 + }); + bd.append(rail); + if (railh) { + railh.css({ + position: "absolute", + left: 0, + bottom: 0 + }); + (railh.align) ? railh.css({ + bottom: 0 + }): railh.css({ + top: 0 + }); + bd.append(railh); + } + } else { + self.isfixed = (self.win.css("position") == "fixed"); + var rlpos = (self.isfixed) ? "fixed" : "absolute"; + + if (!self.isfixed) self.viewport = self.getViewport(self.win[0]); + if (self.viewport) { + self.body = self.viewport; + if ((/fixed|absolute/.test(self.viewport.css("position"))) == false) self.css(self.viewport, { + "position": "relative" + }); + } + + rail.css({ + position: rlpos + }); + if (self.zoom) self.zoom.css({ + position: rlpos + }); + self.updateScrollBar(); + self.body.append(rail); + if (self.zoom) self.body.append(self.zoom); + if (self.railh) { + railh.css({ + position: rlpos + }); + self.body.append(railh); + } + } + + if (cap.isios) self.css(self.win, { + '-webkit-tap-highlight-color': 'rgba(0,0,0,0)', + '-webkit-touch-callout': 'none' + }); // prevent grey layer on click + + if (cap.isie && self.opt.disableoutline) self.win.attr("hideFocus", "true"); // IE, prevent dotted rectangle on focused div + if (cap.iswebkit && self.opt.disableoutline) self.win.css({"outline": "none"}); // Webkit outline + //if (cap.isopera&&self.opt.disableoutline) self.win.css({"outline":"0"}); // Opera 12- to test [TODO] + + } + + if (self.opt.autohidemode === false) { + self.autohidedom = false; + self.rail.css({ + opacity: self.opt.cursoropacitymax + }); + if (self.railh) self.railh.css({ + opacity: self.opt.cursoropacitymax + }); + } else if ((self.opt.autohidemode === true) || (self.opt.autohidemode === "leave")) { + self.autohidedom = $().add(self.rail); + if (cap.isie8) self.autohidedom = self.autohidedom.add(self.cursor); + if (self.railh) self.autohidedom = self.autohidedom.add(self.railh); + if (self.railh && cap.isie8) self.autohidedom = self.autohidedom.add(self.cursorh); + } else if (self.opt.autohidemode == "scroll") { + self.autohidedom = $().add(self.rail); + if (self.railh) self.autohidedom = self.autohidedom.add(self.railh); + } else if (self.opt.autohidemode == "cursor") { + self.autohidedom = $().add(self.cursor); + if (self.railh) self.autohidedom = self.autohidedom.add(self.cursorh); + } else if (self.opt.autohidemode == "hidden") { + self.autohidedom = false; + self.hide(); + self.railslocked = false; + } + + if (cap.isie9mobile) { + + self.scrollmom = new ScrollMomentumClass2D(self); + + self.onmangotouch = function() { + var py = self.getScrollTop(); + var px = self.getScrollLeft(); + + if ((py == self.scrollmom.lastscrolly) && (px == self.scrollmom.lastscrollx)) return true; + + var dfy = py - self.mangotouch.sy; + var dfx = px - self.mangotouch.sx; + var df = Math.round(Math.sqrt(Math.pow(dfx, 2) + Math.pow(dfy, 2))); + if (df == 0) return; + + var dry = (dfy < 0) ? -1 : 1; + var drx = (dfx < 0) ? -1 : 1; + + var tm = +new Date(); + if (self.mangotouch.lazy) clearTimeout(self.mangotouch.lazy); + + if (((tm - self.mangotouch.tm) > 80) || (self.mangotouch.dry != dry) || (self.mangotouch.drx != drx)) { + self.scrollmom.stop(); + self.scrollmom.reset(px, py); + self.mangotouch.sy = py; + self.mangotouch.ly = py; + self.mangotouch.sx = px; + self.mangotouch.lx = px; + self.mangotouch.dry = dry; + self.mangotouch.drx = drx; + self.mangotouch.tm = tm; + } else { + + self.scrollmom.stop(); + self.scrollmom.update(self.mangotouch.sx - dfx, self.mangotouch.sy - dfy); + self.mangotouch.tm = tm; + + var ds = Math.max(Math.abs(self.mangotouch.ly - py), Math.abs(self.mangotouch.lx - px)); + self.mangotouch.ly = py; + self.mangotouch.lx = px; + + if (ds > 2) { + self.mangotouch.lazy = setTimeout(function() { + self.mangotouch.lazy = false; + self.mangotouch.dry = 0; + self.mangotouch.drx = 0; + self.mangotouch.tm = 0; + self.scrollmom.doMomentum(30); + }, 100); + } + } + }; + + var top = self.getScrollTop(); + var lef = self.getScrollLeft(); + self.mangotouch = { + sy: top, + ly: top, + dry: 0, + sx: lef, + lx: lef, + drx: 0, + lazy: false, + tm: 0 + }; + + self.bind(self.docscroll, "scroll", self.onmangotouch); + + } else { + + if (cap.cantouch || self.istouchcapable || self.opt.touchbehavior || cap.hasmstouch) { + + self.scrollmom = new ScrollMomentumClass2D(self); + + self.ontouchstart = function(e) { + if (e.pointerType && e.pointerType != 2 && e.pointerType != "touch") return false; + + self.hasmoving = false; + + if (!self.railslocked) { + + var tg; + if (cap.hasmstouch) { + tg = (e.target) ? e.target : false; + while (tg) { + var nc = $(tg).getNiceScroll(); + if ((nc.length > 0) && (nc[0].me == self.me)) break; + if (nc.length > 0) return false; + if ((tg.nodeName == 'DIV') && (tg.id == self.id)) break; + tg = (tg.parentNode) ? tg.parentNode : false; + } + } + + self.cancelScroll(); + + tg = self.getTarget(e); + + if (tg) { + var skp = (/INPUT/i.test(tg.nodeName)) && (/range/i.test(tg.type)); + if (skp) return self.stopPropagation(e); + } + + if (!("clientX" in e) && ("changedTouches" in e)) { + e.clientX = e.changedTouches[0].clientX; + e.clientY = e.changedTouches[0].clientY; + } + + if (self.forcescreen) { + var le = e; + e = { + "original": (e.original) ? e.original : e + }; + e.clientX = le.screenX; + e.clientY = le.screenY; + } + + self.rail.drag = { + x: e.clientX, + y: e.clientY, + sx: self.scroll.x, + sy: self.scroll.y, + st: self.getScrollTop(), + sl: self.getScrollLeft(), + pt: 2, + dl: false + }; + + if (self.ispage || !self.opt.directionlockdeadzone) { + self.rail.drag.dl = "f"; + } else { + + var view = { + w: $(window).width(), + h: $(window).height() + }; + + var page = { + w: Math.max(document.body.scrollWidth, document.documentElement.scrollWidth), + h: Math.max(document.body.scrollHeight, document.documentElement.scrollHeight) + }; + + var maxh = Math.max(0, page.h - view.h); + var maxw = Math.max(0, page.w - view.w); + + if (!self.rail.scrollable && self.railh.scrollable) self.rail.drag.ck = (maxh > 0) ? "v" : false; + else if (self.rail.scrollable && !self.railh.scrollable) self.rail.drag.ck = (maxw > 0) ? "h" : false; + else self.rail.drag.ck = false; + if (!self.rail.drag.ck) self.rail.drag.dl = "f"; + } + + if (self.opt.touchbehavior && self.isiframe && cap.isie) { + var wp = self.win.position(); + self.rail.drag.x += wp.left; + self.rail.drag.y += wp.top; + } + + self.hasmoving = false; + self.lastmouseup = false; + self.scrollmom.reset(e.clientX, e.clientY); + + if (!cap.cantouch && !this.istouchcapable && !e.pointerType) { + + var ip = (tg) ? /INPUT|SELECT|TEXTAREA/i.test(tg.nodeName) : false; + if (!ip) { + if (!self.ispage && cap.hasmousecapture) tg.setCapture(); + if (self.opt.touchbehavior) { + if (tg.onclick && !(tg._onclick || false)) { // intercept DOM0 onclick event + tg._onclick = tg.onclick; + tg.onclick = function(e) { + if (self.hasmoving) return false; + tg._onclick.call(this, e); + }; + } + return self.cancelEvent(e); + } + return self.stopPropagation(e); + } + + if (/SUBMIT|CANCEL|BUTTON/i.test($(tg).attr('type'))) { + pc = { + "tg": tg, + "click": false + }; + self.preventclick = pc; + } + + } + } + + }; + + self.ontouchend = function(e) { + if (!self.rail.drag) return true; + if (self.rail.drag.pt == 2) { + if (e.pointerType && e.pointerType != 2 && e.pointerType != "touch") return false; + self.scrollmom.doMomentum(); + self.rail.drag = false; + if (self.hasmoving) { + self.lastmouseup = true; + self.hideCursor(); + if (cap.hasmousecapture) document.releaseCapture(); + if (!cap.cantouch) return self.cancelEvent(e); + } + } + else if (self.rail.drag.pt == 1) { + return self.onmouseup(e); + } + + }; + + var moveneedoffset = (self.opt.touchbehavior && self.isiframe && !cap.hasmousecapture); + + self.ontouchmove = function(e, byiframe) { + + if (!self.rail.drag) return false; + + if (e.targetTouches && self.opt.preventmultitouchscrolling) { + if (e.targetTouches.length > 1) return false; // multitouch + } + + if (e.pointerType && e.pointerType != 2 && e.pointerType != "touch") return false; + + if (self.rail.drag.pt == 2) { + if (cap.cantouch && (cap.isios) && (typeof e.original == "undefined")) return true; // prevent ios "ghost" events by clickable elements + + self.hasmoving = true; + + if (self.preventclick && !self.preventclick.click) { + self.preventclick.click = self.preventclick.tg.onclick || false; + self.preventclick.tg.onclick = self.onpreventclick; + } + + var ev = $.extend({ + "original": e + }, e); + e = ev; + + if (("changedTouches" in e)) { + e.clientX = e.changedTouches[0].clientX; + e.clientY = e.changedTouches[0].clientY; + } + + if (self.forcescreen) { + var le = e; + e = { + "original": (e.original) ? e.original : e + }; + e.clientX = le.screenX; + e.clientY = le.screenY; + } + + var ofy,ofx; + ofx = ofy = 0; + + if (moveneedoffset && !byiframe) { + var wp = self.win.position(); + ofx = -wp.left; + ofy = -wp.top; + } + + var fy = e.clientY + ofy; + var my = (fy - self.rail.drag.y); + var fx = e.clientX + ofx; + var mx = (fx - self.rail.drag.x); + + var ny = self.rail.drag.st - my; + + if (self.ishwscroll && self.opt.bouncescroll) { + if (ny < 0) { + ny = Math.round(ny / 2); + // fy = 0; + } else if (ny > self.page.maxh) { + ny = self.page.maxh + Math.round((ny - self.page.maxh) / 2); + // fy = 0; + } + } else { + if (ny < 0) { + ny = 0; + fy = 0; + } + if (ny > self.page.maxh) { + ny = self.page.maxh; + fy = 0; + } + } + + var nx; + if (self.railh && self.railh.scrollable) { + nx = (self.isrtlmode) ? mx - self.rail.drag.sl : self.rail.drag.sl - mx; + + if (self.ishwscroll && self.opt.bouncescroll) { + if (nx < 0) { + nx = Math.round(nx / 2); + // fx = 0; + } else if (nx > self.page.maxw) { + nx = self.page.maxw + Math.round((nx - self.page.maxw) / 2); + // fx = 0; + } + } else { + if (nx < 0) { + nx = 0; + fx = 0; + } + if (nx > self.page.maxw) { + nx = self.page.maxw; + fx = 0; + } + } + + } + + var grabbed = false; + if (self.rail.drag.dl) { + grabbed = true; + if (self.rail.drag.dl == "v") nx = self.rail.drag.sl; + else if (self.rail.drag.dl == "h") ny = self.rail.drag.st; + } else { + var ay = Math.abs(my); + var ax = Math.abs(mx); + var dz = self.opt.directionlockdeadzone; + if (self.rail.drag.ck == "v") { + if (ay > dz && (ax <= (ay * 0.3))) { + self.rail.drag = false; + return true; + } else if (ax > dz) { + self.rail.drag.dl = "f"; + $("body").scrollTop($("body").scrollTop()); // stop iOS native scrolling (when active javascript has blocked) + } + } else if (self.rail.drag.ck == "h") { + if (ax > dz && (ay <= (ax * 0.3))) { + self.rail.drag = false; + return true; + } else if (ay > dz) { + self.rail.drag.dl = "f"; + $("body").scrollLeft($("body").scrollLeft()); // stop iOS native scrolling (when active javascript has blocked) + } + } + } + + self.synched("touchmove", function() { + if (self.rail.drag && (self.rail.drag.pt == 2)) { + if (self.prepareTransition) self.prepareTransition(0); + if (self.rail.scrollable) self.setScrollTop(ny); + self.scrollmom.update(fx, fy); + if (self.railh && self.railh.scrollable) { + self.setScrollLeft(nx); + self.showCursor(ny, nx); + } else { + self.showCursor(ny); + } + if (cap.isie10) document.selection.clear(); + } + }); + + if (cap.ischrome && self.istouchcapable) grabbed = false; //chrome touch emulation doesn't like! + if (grabbed) return self.cancelEvent(e); + } + else if (self.rail.drag.pt == 1) { // drag on cursor + return self.onmousemove(e); + } + + }; + + } + + self.onmousedown = function(e, hronly) { + if (self.rail.drag && self.rail.drag.pt != 1) return; + if (self.railslocked) return self.cancelEvent(e); + self.cancelScroll(); + self.rail.drag = { + x: e.clientX, + y: e.clientY, + sx: self.scroll.x, + sy: self.scroll.y, + pt: 1, + hr: (!!hronly) + }; + var tg = self.getTarget(e); + if (!self.ispage && cap.hasmousecapture) tg.setCapture(); + if (self.isiframe && !cap.hasmousecapture) { + self.saved.csspointerevents = self.doc.css("pointer-events"); + self.css(self.doc, { + "pointer-events": "none" + }); + } + self.hasmoving = false; + return self.cancelEvent(e); + }; + + self.onmouseup = function(e) { + if (self.rail.drag) { + if (self.rail.drag.pt != 1) return true; + if (cap.hasmousecapture) document.releaseCapture(); + if (self.isiframe && !cap.hasmousecapture) self.doc.css("pointer-events", self.saved.csspointerevents); + self.rail.drag = false; + //if (!self.rail.active) self.hideCursor(); + if (self.hasmoving) self.triggerScrollEnd(); // TODO - check &&!self.scrollrunning + return self.cancelEvent(e); + } + }; + + self.onmousemove = function(e) { + if (self.rail.drag) { + if (self.rail.drag.pt != 1) return; + + if (cap.ischrome && e.which == 0) return self.onmouseup(e); + + self.cursorfreezed = true; + self.hasmoving = true; + + if (self.rail.drag.hr) { + self.scroll.x = self.rail.drag.sx + (e.clientX - self.rail.drag.x); + if (self.scroll.x < 0) self.scroll.x = 0; + var mw = self.scrollvaluemaxw; + if (self.scroll.x > mw) self.scroll.x = mw; + } else { + self.scroll.y = self.rail.drag.sy + (e.clientY - self.rail.drag.y); + if (self.scroll.y < 0) self.scroll.y = 0; + var my = self.scrollvaluemax; + if (self.scroll.y > my) self.scroll.y = my; + } + + self.synched('mousemove', function() { + if (self.rail.drag && (self.rail.drag.pt == 1)) { + self.showCursor(); + if (self.rail.drag.hr) { + if (self.hasreversehr) { + self.doScrollLeft(self.scrollvaluemaxw-Math.round(self.scroll.x * self.scrollratio.x), self.opt.cursordragspeed); + } else { + self.doScrollLeft(Math.round(self.scroll.x * self.scrollratio.x), self.opt.cursordragspeed); + } + } + else self.doScrollTop(Math.round(self.scroll.y * self.scrollratio.y), self.opt.cursordragspeed); + } + }); + + return self.cancelEvent(e); + } + /* + else { + self.checkarea = true; + } +*/ + }; + + if (cap.cantouch || self.opt.touchbehavior) { + + self.onpreventclick = function(e) { + if (self.preventclick) { + self.preventclick.tg.onclick = self.preventclick.click; + self.preventclick = false; + return self.cancelEvent(e); + } + } + + self.bind(self.win, "mousedown", self.ontouchstart); // control content dragging + + self.onclick = (cap.isios) ? false : function(e) { + if (self.lastmouseup) { + self.lastmouseup = false; + return self.cancelEvent(e); + } else { + return true; + } + }; + + if (self.opt.grabcursorenabled && cap.cursorgrabvalue) { + self.css((self.ispage) ? self.doc : self.win, { + 'cursor': cap.cursorgrabvalue + }); + self.css(self.rail, { + 'cursor': cap.cursorgrabvalue + }); + } + + } else { + + var checkSelectionScroll = function(e) { + if (!self.selectiondrag) return; + + if (e) { + var ww = self.win.outerHeight(); + var df = (e.pageY - self.selectiondrag.top); + if (df > 0 && df < ww) df = 0; + if (df >= ww) df -= ww; + self.selectiondrag.df = df; + } + if (self.selectiondrag.df == 0) return; + + var rt = -Math.floor(self.selectiondrag.df / 6) * 2; + self.doScrollBy(rt); + + self.debounced("doselectionscroll", function() { + checkSelectionScroll() + }, 50); + }; + + if ("getSelection" in document) { // A grade - Major browsers + self.hasTextSelected = function() { + return (document.getSelection().rangeCount > 0); + }; + } else if ("selection" in document) { //IE9- + self.hasTextSelected = function() { + return (document.selection.type != "None"); + }; + } else { + self.hasTextSelected = function() { // no support + return false; + }; + } + + self.onselectionstart = function(e) { +/* More testing - severe chrome issues + if (!self.haswrapper&&(e.which&&e.which==2)) { // fool browser to manage middle button scrolling + self.win.css({'overflow':'auto'}); + setTimeout(function(){ + self.win.css({'overflow':''}); + },10); + return true; + } +*/ + if (self.ispage) return; + self.selectiondrag = self.win.offset(); + }; + + self.onselectionend = function(e) { + self.selectiondrag = false; + }; + self.onselectiondrag = function(e) { + if (!self.selectiondrag) return; + if (self.hasTextSelected()) self.debounced("selectionscroll", function() { + checkSelectionScroll(e) + }, 250); + }; + + + } + + if (cap.hasw3ctouch) { //IE11+ + self.css(self.rail, { + 'touch-action': 'none' + }); + self.css(self.cursor, { + 'touch-action': 'none' + }); + self.bind(self.win, "pointerdown", self.ontouchstart); + self.bind(document, "pointerup", self.ontouchend); + self.bind(document, "pointermove", self.ontouchmove); + } else if (cap.hasmstouch) { //IE10 + self.css(self.rail, { + '-ms-touch-action': 'none' + }); + self.css(self.cursor, { + '-ms-touch-action': 'none' + }); + self.bind(self.win, "MSPointerDown", self.ontouchstart); + self.bind(document, "MSPointerUp", self.ontouchend); + self.bind(document, "MSPointerMove", self.ontouchmove); + self.bind(self.cursor, "MSGestureHold", function(e) { + e.preventDefault() + }); + self.bind(self.cursor, "contextmenu", function(e) { + e.preventDefault() + }); + } else if (this.istouchcapable) { //desktop with screen touch enabled + self.bind(self.win, "touchstart", self.ontouchstart); + self.bind(document, "touchend", self.ontouchend); + self.bind(document, "touchcancel", self.ontouchend); + self.bind(document, "touchmove", self.ontouchmove); + } + + + if (self.opt.cursordragontouch || (!cap.cantouch && !self.opt.touchbehavior)) { + + self.rail.css({ + "cursor": "default" + }); + self.railh && self.railh.css({ + "cursor": "default" + }); + + self.jqbind(self.rail, "mouseenter", function() { + if (!self.ispage && !self.win.is(":visible")) return false; + if (self.canshowonmouseevent) self.showCursor(); + self.rail.active = true; + }); + self.jqbind(self.rail, "mouseleave", function() { + self.rail.active = false; + if (!self.rail.drag) self.hideCursor(); + }); + + if (self.opt.sensitiverail) { + self.bind(self.rail, "click", function(e) { + self.doRailClick(e, false, false) + }); + self.bind(self.rail, "dblclick", function(e) { + self.doRailClick(e, true, false) + }); + self.bind(self.cursor, "click", function(e) { + self.cancelEvent(e) + }); + self.bind(self.cursor, "dblclick", function(e) { + self.cancelEvent(e) + }); + } + + if (self.railh) { + self.jqbind(self.railh, "mouseenter", function() { + if (!self.ispage && !self.win.is(":visible")) return false; + if (self.canshowonmouseevent) self.showCursor(); + self.rail.active = true; + }); + self.jqbind(self.railh, "mouseleave", function() { + self.rail.active = false; + if (!self.rail.drag) self.hideCursor(); + }); + + if (self.opt.sensitiverail) { + self.bind(self.railh, "click", function(e) { + self.doRailClick(e, false, true) + }); + self.bind(self.railh, "dblclick", function(e) { + self.doRailClick(e, true, true) + }); + self.bind(self.cursorh, "click", function(e) { + self.cancelEvent(e) + }); + self.bind(self.cursorh, "dblclick", function(e) { + self.cancelEvent(e) + }); + } + + } + + } + + if (!cap.cantouch && !self.opt.touchbehavior) { + + self.bind((cap.hasmousecapture) ? self.win : document, "mouseup", self.onmouseup); + self.bind(document, "mousemove", self.onmousemove); + if (self.onclick) self.bind(document, "click", self.onclick); + + self.bind(self.cursor, "mousedown", self.onmousedown); + self.bind(self.cursor, "mouseup", self.onmouseup); + + if (self.railh) { + self.bind(self.cursorh, "mousedown", function(e) { + self.onmousedown(e, true) + }); + self.bind(self.cursorh, "mouseup", self.onmouseup); + } + + if (!self.ispage && self.opt.enablescrollonselection) { + self.bind(self.win[0], "mousedown", self.onselectionstart); + self.bind(document, "mouseup", self.onselectionend); + self.bind(self.cursor, "mouseup", self.onselectionend); + if (self.cursorh) self.bind(self.cursorh, "mouseup", self.onselectionend); + self.bind(document, "mousemove", self.onselectiondrag); + } + + if (self.zoom) { + self.jqbind(self.zoom, "mouseenter", function() { + if (self.canshowonmouseevent) self.showCursor(); + self.rail.active = true; + }); + self.jqbind(self.zoom, "mouseleave", function() { + self.rail.active = false; + if (!self.rail.drag) self.hideCursor(); + }); + } + + } else { + + self.bind((cap.hasmousecapture) ? self.win : document, "mouseup", self.ontouchend); + self.bind(document, "mousemove", self.ontouchmove); + if (self.onclick) self.bind(document, "click", self.onclick); + + if (self.opt.cursordragontouch) { + self.bind(self.cursor, "mousedown", self.onmousedown); + self.bind(self.cursor, "mouseup", self.onmouseup); + //self.bind(self.cursor, "mousemove", self.onmousemove); + self.cursorh && self.bind(self.cursorh, "mousedown", function(e) { + self.onmousedown(e, true) + }); + //self.cursorh && self.bind(self.cursorh, "mousemove", self.onmousemove); + self.cursorh && self.bind(self.cursorh, "mouseup", self.onmouseup); + } + + } + + if (self.opt.enablemousewheel) { + if (!self.isiframe) self.bind((cap.isie && self.ispage) ? document : self.win /*self.docscroll*/ , "mousewheel", self.onmousewheel); + self.bind(self.rail, "mousewheel", self.onmousewheel); + if (self.railh) self.bind(self.railh, "mousewheel", self.onmousewheelhr); + } + + if (!self.ispage && !cap.cantouch && !(/HTML|^BODY/.test(self.win[0].nodeName))) { + if (!self.win.attr("tabindex")) self.win.attr({ + "tabindex": tabindexcounter++ + }); + + self.jqbind(self.win, "focus", function(e) { + domfocus = (self.getTarget(e)).id || true; + self.hasfocus = true; + if (self.canshowonmouseevent) self.noticeCursor(); + }); + self.jqbind(self.win, "blur", function(e) { + domfocus = false; + self.hasfocus = false; + }); + + self.jqbind(self.win, "mouseenter", function(e) { + mousefocus = (self.getTarget(e)).id || true; + self.hasmousefocus = true; + if (self.canshowonmouseevent) self.noticeCursor(); + }); + self.jqbind(self.win, "mouseleave", function() { + mousefocus = false; + self.hasmousefocus = false; + if (!self.rail.drag) self.hideCursor(); + }); + + } + + } // !ie9mobile + + //Thanks to http://www.quirksmode.org !! + self.onkeypress = function(e) { + if (self.railslocked && self.page.maxh == 0) return true; + + e = (e) ? e : window.e; + var tg = self.getTarget(e); + if (tg && /INPUT|TEXTAREA|SELECT|OPTION/.test(tg.nodeName)) { + var tp = tg.getAttribute('type') || tg.type || false; + if ((!tp) || !(/submit|button|cancel/i.tp)) return true; + } + + if ($(tg).attr('contenteditable')) return true; + + if (self.hasfocus || (self.hasmousefocus && !domfocus) || (self.ispage && !domfocus && !mousefocus)) { + var key = e.keyCode; + + if (self.railslocked && key != 27) return self.cancelEvent(e); + + var ctrl = e.ctrlKey || false; + var shift = e.shiftKey || false; + + var ret = false; + switch (key) { + case 38: + case 63233: //safari + self.doScrollBy(24 * 3); + ret = true; + break; + case 40: + case 63235: //safari + self.doScrollBy(-24 * 3); + ret = true; + break; + case 37: + case 63232: //safari + if (self.railh) { + (ctrl) ? self.doScrollLeft(0): self.doScrollLeftBy(24 * 3); + ret = true; + } + break; + case 39: + case 63234: //safari + if (self.railh) { + (ctrl) ? self.doScrollLeft(self.page.maxw): self.doScrollLeftBy(-24 * 3); + ret = true; + } + break; + case 33: + case 63276: // safari + self.doScrollBy(self.view.h); + ret = true; + break; + case 34: + case 63277: // safari + self.doScrollBy(-self.view.h); + ret = true; + break; + case 36: + case 63273: // safari + (self.railh && ctrl) ? self.doScrollPos(0, 0): self.doScrollTo(0); + ret = true; + break; + case 35: + case 63275: // safari + (self.railh && ctrl) ? self.doScrollPos(self.page.maxw, self.page.maxh): self.doScrollTo(self.page.maxh); + ret = true; + break; + case 32: + if (self.opt.spacebarenabled) { + (shift) ? self.doScrollBy(self.view.h): self.doScrollBy(-self.view.h); + ret = true; + } + break; + case 27: // ESC + if (self.zoomactive) { + self.doZoom(); + ret = true; + } + break; + } + if (ret) return self.cancelEvent(e); + } + }; + + if (self.opt.enablekeyboard) self.bind(document, (cap.isopera && !cap.isopera12) ? "keypress" : "keydown", self.onkeypress); + + self.bind(document, "keydown", function(e) { + var ctrl = e.ctrlKey || false; + if (ctrl) self.wheelprevented = true; + }); + self.bind(document, "keyup", function(e) { + var ctrl = e.ctrlKey || false; + if (!ctrl) self.wheelprevented = false; + }); + self.bind(window,"blur",function(e){ + self.wheelprevented = false; + }); + + self.bind(window, 'resize', self.lazyResize); + self.bind(window, 'orientationchange', self.lazyResize); + + self.bind(window, "load", self.lazyResize); + + if (cap.ischrome && !self.ispage && !self.haswrapper) { //chrome void scrollbar bug - it persists in version 26 + var tmp = self.win.attr("style"); + var ww = parseFloat(self.win.css("width")) + 1; + self.win.css('width', ww); + self.synched("chromefix", function() { + self.win.attr("style", tmp) + }); + } + + + // Trying a cross-browser implementation - good luck! + + self.onAttributeChange = function(e) { + self.lazyResize(self.isieold ? 250 : 30); + }; + + if (ClsMutationObserver !== false) { + self.observerbody = new ClsMutationObserver(function(mutations) { + mutations.forEach(function(mut){ + if (mut.type=="attributes") { + return ($("body").hasClass("modal-open")) ? self.hide() : self.show(); // Support for Bootstrap modal + } + }); + if (document.body.scrollHeight!=self.page.maxh) return self.lazyResize(30); + }); + self.observerbody.observe(document.body, { + childList: true, + subtree: true, + characterData: false, + attributes: true, + attributeFilter: ['class'] + }); + } + + if (!self.ispage && !self.haswrapper) { + // redesigned MutationObserver for Chrome18+/Firefox14+/iOS6+ with support for: remove div, add/remove content + if (ClsMutationObserver !== false) { + self.observer = new ClsMutationObserver(function(mutations) { + mutations.forEach(self.onAttributeChange); + }); + self.observer.observe(self.win[0], { + childList: true, + characterData: false, + attributes: true, + subtree: false + }); + self.observerremover = new ClsMutationObserver(function(mutations) { + mutations.forEach(function(mo) { + if (mo.removedNodes.length > 0) { + for (var dd in mo.removedNodes) { + if (!!self && (mo.removedNodes[dd] == self.win[0])) return self.remove(); + } + } + }); + }); + self.observerremover.observe(self.win[0].parentNode, { + childList: true, + characterData: false, + attributes: false, + subtree: false + }); + } else { + self.bind(self.win, (cap.isie && !cap.isie9) ? "propertychange" : "DOMAttrModified", self.onAttributeChange); + if (cap.isie9) self.win[0].attachEvent("onpropertychange", self.onAttributeChange); //IE9 DOMAttrModified bug + self.bind(self.win, "DOMNodeRemoved", function(e) { + if (e.target == self.win[0]) self.remove(); + }); + } + } + + // + + if (!self.ispage && self.opt.boxzoom) self.bind(window, "resize", self.resizeZoom); + if (self.istextarea) self.bind(self.win, "mouseup", self.lazyResize); + + // self.checkrtlmode = true; + self.lazyResize(30); + + } + + if (this.doc[0].nodeName == 'IFRAME') { + var oniframeload = function() { + self.iframexd = false; + var doc; + try { + doc = 'contentDocument' in this ? this.contentDocument : this.contentWindow.document; + var a = doc.domain; + } catch (e) { + self.iframexd = true; + doc = false + } + + if (self.iframexd) { + if ("console" in window) console.log('NiceScroll error: policy restriced iframe'); + return true; //cross-domain - I can't manage this + } + + self.forcescreen = true; + + if (self.isiframe) { + self.iframe = { + "doc": $(doc), + "html": self.doc.contents().find('html')[0], + "body": self.doc.contents().find('body')[0] + }; + self.getContentSize = function() { + return { + w: Math.max(self.iframe.html.scrollWidth, self.iframe.body.scrollWidth), + h: Math.max(self.iframe.html.scrollHeight, self.iframe.body.scrollHeight) + }; + }; + self.docscroll = $(self.iframe.body); //$(this.contentWindow); + } + + if (!cap.isios && self.opt.iframeautoresize && !self.isiframe) { + self.win.scrollTop(0); // reset position + self.doc.height(""); //reset height to fix browser bug + var hh = Math.max(doc.getElementsByTagName('html')[0].scrollHeight, doc.body.scrollHeight); + self.doc.height(hh); + } + self.lazyResize(30); + + if (cap.isie7) self.css($(self.iframe.html), { + 'overflow-y': 'hidden' + }); + self.css($(self.iframe.body), { + 'overflow-y': 'hidden' + }); + + if (cap.isios && self.haswrapper) { + self.css($(doc.body), { + '-webkit-transform': 'translate3d(0,0,0)' + }); // avoid iFrame content clipping - thanks to http://blog.derraab.com/2012/04/02/avoid-iframe-content-clipping-with-css-transform-on-ios/ + } + + if ('contentWindow' in this) { + self.bind(this.contentWindow, "scroll", self.onscroll); //IE8 & minor + } else { + self.bind(doc, "scroll", self.onscroll); + } + + if (self.opt.enablemousewheel) { + self.bind(doc, "mousewheel", self.onmousewheel); + } + + if (self.opt.enablekeyboard) self.bind(doc, (cap.isopera) ? "keypress" : "keydown", self.onkeypress); + + if (cap.cantouch || self.opt.touchbehavior) { + self.bind(doc, "mousedown", self.ontouchstart); + self.bind(doc, "mousemove", function(e) { + return self.ontouchmove(e, true) + }); + if (self.opt.grabcursorenabled && cap.cursorgrabvalue) self.css($(doc.body), { + 'cursor': cap.cursorgrabvalue + }); + } + + self.bind(doc, "mouseup", self.ontouchend); + + if (self.zoom) { + if (self.opt.dblclickzoom) self.bind(doc, 'dblclick', self.doZoom); + if (self.ongesturezoom) self.bind(doc, "gestureend", self.ongesturezoom); + } + }; + + if (this.doc[0].readyState && this.doc[0].readyState == "complete") { + setTimeout(function() { + oniframeload.call(self.doc[0], false) + }, 500); + } + self.bind(this.doc, "load", oniframeload); + + } + + }; + + this.showCursor = function(py, px) { + if (self.cursortimeout) { + clearTimeout(self.cursortimeout); + self.cursortimeout = 0; + } + if (!self.rail) return; + if (self.autohidedom) { + self.autohidedom.stop().css({ + opacity: self.opt.cursoropacitymax + }); + self.cursoractive = true; + } + + if (!self.rail.drag || self.rail.drag.pt != 1) { + if ((typeof py != "undefined") && (py !== false)) { + self.scroll.y = Math.round(py * 1 / self.scrollratio.y); + } + if (typeof px != "undefined") { + self.scroll.x = Math.round(px * 1 / self.scrollratio.x); + } + } + + self.cursor.css({ + height: self.cursorheight, + top: self.scroll.y + }); + if (self.cursorh) { + var lx = (self.hasreversehr) ? self.scrollvaluemaxw-self.scroll.x : self.scroll.x; + (!self.rail.align && self.rail.visibility) ? self.cursorh.css({ + width: self.cursorwidth, + left: lx + self.rail.width + }): self.cursorh.css({ + width: self.cursorwidth, + left: lx + }); + self.cursoractive = true; + } + + if (self.zoom) self.zoom.stop().css({ + opacity: self.opt.cursoropacitymax + }); + }; + + this.hideCursor = function(tm) { + if (self.cursortimeout) return; + if (!self.rail) return; + if (!self.autohidedom) return; + if (self.hasmousefocus && self.opt.autohidemode == "leave") return; + self.cursortimeout = setTimeout(function() { + if (!self.rail.active || !self.showonmouseevent) { + self.autohidedom.stop().animate({ + opacity: self.opt.cursoropacitymin + }); + if (self.zoom) self.zoom.stop().animate({ + opacity: self.opt.cursoropacitymin + }); + self.cursoractive = false; + } + self.cursortimeout = 0; + }, tm || self.opt.hidecursordelay); + }; + + this.noticeCursor = function(tm, py, px) { + self.showCursor(py, px); + if (!self.rail.active) self.hideCursor(tm); + }; + + this.getContentSize = + (self.ispage) ? + function() { + return { + w: Math.max(document.body.scrollWidth, document.documentElement.scrollWidth), + h: Math.max(document.body.scrollHeight, document.documentElement.scrollHeight) + } + } : (self.haswrapper) ? + function() { + return { + w: self.doc.outerWidth() + parseInt(self.win.css('paddingLeft')) + parseInt(self.win.css('paddingRight')), + h: self.doc.outerHeight() + parseInt(self.win.css('paddingTop')) + parseInt(self.win.css('paddingBottom')) + } + } : function() { + return { + w: self.docscroll[0].scrollWidth, + h: self.docscroll[0].scrollHeight + } + }; + + this.onResize = function(e, page) { + + if (!self || !self.win) return false; + + if (!self.haswrapper && !self.ispage) { + if (self.win.css('display') == 'none') { + if (self.visibility) self.hideRail().hideRailHr(); + return false; + } else { + if (!self.hidden && !self.visibility) self.showRail().showRailHr(); + } + } + + var premaxh = self.page.maxh; + var premaxw = self.page.maxw; + + var preview = { + h: self.view.h, + w: self.view.w + }; + + self.view = { + w: (self.ispage) ? self.win.width() : parseInt(self.win[0].clientWidth), + h: (self.ispage) ? self.win.height() : parseInt(self.win[0].clientHeight) + }; + + self.page = (page) ? page : self.getContentSize(); + + self.page.maxh = Math.max(0, self.page.h - self.view.h); + self.page.maxw = Math.max(0, self.page.w - self.view.w); + + if ((self.page.maxh == premaxh) && (self.page.maxw == premaxw) && (self.view.w == preview.w) && (self.view.h == preview.h)) { + // test position + if (!self.ispage) { + var pos = self.win.offset(); + if (self.lastposition) { + var lst = self.lastposition; + if ((lst.top == pos.top) && (lst.left == pos.left)) return self; //nothing to do + } + self.lastposition = pos; + } else { + return self; //nothing to do + } + } + + if (self.page.maxh == 0) { + self.hideRail(); + self.scrollvaluemax = 0; + self.scroll.y = 0; + self.scrollratio.y = 0; + self.cursorheight = 0; + self.setScrollTop(0); + self.rail.scrollable = false; + } else { + self.page.maxh -= (self.opt.railpadding.top + self.opt.railpadding.bottom); //** + self.rail.scrollable = true; + } + + if (self.page.maxw == 0) { + self.hideRailHr(); + self.scrollvaluemaxw = 0; + self.scroll.x = 0; + self.scrollratio.x = 0; + self.cursorwidth = 0; + self.setScrollLeft(0); + self.railh.scrollable = false; + } else { + self.page.maxw -= (self.opt.railpadding.left + self.opt.railpadding.right); //** + self.railh.scrollable = true; + } + + self.railslocked = (self.locked) || ((self.page.maxh == 0) && (self.page.maxw == 0)); + if (self.railslocked) { + if (!self.ispage) self.updateScrollBar(self.view); + return false; + } + + if (!self.hidden && !self.visibility) { + self.showRail().showRailHr(); + } + else if (!self.hidden && !self.railh.visibility) self.showRailHr(); + + if (self.istextarea && self.win.css('resize') && self.win.css('resize') != 'none') self.view.h -= 20; + + self.cursorheight = Math.min(self.view.h, Math.round(self.view.h * (self.view.h / self.page.h))); + self.cursorheight = (self.opt.cursorfixedheight) ? self.opt.cursorfixedheight : Math.max(self.opt.cursorminheight, self.cursorheight); + + self.cursorwidth = Math.min(self.view.w, Math.round(self.view.w * (self.view.w / self.page.w))); + self.cursorwidth = (self.opt.cursorfixedheight) ? self.opt.cursorfixedheight : Math.max(self.opt.cursorminheight, self.cursorwidth); + + self.scrollvaluemax = self.view.h - self.cursorheight - self.cursor.hborder - (self.opt.railpadding.top + self.opt.railpadding.bottom); //** + + if (self.railh) { + self.railh.width = (self.page.maxh > 0) ? (self.view.w - self.rail.width) : self.view.w; + self.scrollvaluemaxw = self.railh.width - self.cursorwidth - self.cursorh.wborder - (self.opt.railpadding.left + self.opt.railpadding.right); //** + } + + /* + if (self.checkrtlmode&&self.railh) { + self.checkrtlmode = false; + if (self.opt.rtlmode&&self.scroll.x==0) self.setScrollLeft(self.page.maxw); + } +*/ + + if (!self.ispage) self.updateScrollBar(self.view); + + self.scrollratio = { + x: (self.page.maxw / self.scrollvaluemaxw), + y: (self.page.maxh / self.scrollvaluemax) + }; + + var sy = self.getScrollTop(); + if (sy > self.page.maxh) { + self.doScrollTop(self.page.maxh); + } else { + self.scroll.y = Math.round(self.getScrollTop() * (1 / self.scrollratio.y)); + self.scroll.x = Math.round(self.getScrollLeft() * (1 / self.scrollratio.x)); + if (self.cursoractive) self.noticeCursor(); + } + + if (self.scroll.y && (self.getScrollTop() == 0)) self.doScrollTo(Math.floor(self.scroll.y * self.scrollratio.y)); + + return self; + }; + + this.resize = self.onResize; + + this.lazyResize = function(tm) { // event debounce + tm = (isNaN(tm)) ? 30 : tm; + self.debounced('resize', self.resize, tm); + return self; + }; + + // modified by MDN https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/wheel + function _modernWheelEvent(dom, name, fn, bubble) { + self._bind(dom, name, function(e) { + var e = (e) ? e : window.event; + var event = { + original: e, + target: e.target || e.srcElement, + type: "wheel", + deltaMode: e.type == "MozMousePixelScroll" ? 0 : 1, + deltaX: 0, + deltaZ: 0, + preventDefault: function() { + e.preventDefault ? e.preventDefault() : e.returnValue = false; + return false; + }, + stopImmediatePropagation: function() { + (e.stopImmediatePropagation) ? e.stopImmediatePropagation(): e.cancelBubble = true; + } + }; + + if (name == "mousewheel") { + event.deltaY = -1 / 40 * e.wheelDelta; + e.wheelDeltaX && (event.deltaX = -1 / 40 * e.wheelDeltaX); + } else { + event.deltaY = e.detail; + } + + return fn.call(dom, event); + }, bubble); + }; + + + + this.jqbind = function(dom, name, fn) { // use jquery bind for non-native events (mouseenter/mouseleave) + self.events.push({ + e: dom, + n: name, + f: fn, + q: true + }); + $(dom).bind(name, fn); + }; + + this.bind = function(dom, name, fn, bubble) { // touch-oriented & fixing jquery bind + var el = ("jquery" in dom) ? dom[0] : dom; + + if (name == 'mousewheel') { + if (window.addEventListener||'onwheel' in document) { // modern brosers & IE9 detection fix + self._bind(el, "wheel", fn, bubble || false); + } else { + var wname = (typeof document.onmousewheel != "undefined") ? "mousewheel" : "DOMMouseScroll"; // older IE/Firefox + _modernWheelEvent(el, wname, fn, bubble || false); + if (wname == "DOMMouseScroll") _modernWheelEvent(el, "MozMousePixelScroll", fn, bubble || false); // Firefox legacy + } + } else if (el.addEventListener) { + if (cap.cantouch && /mouseup|mousedown|mousemove/.test(name)) { // touch device support + var tt = (name == 'mousedown') ? 'touchstart' : (name == 'mouseup') ? 'touchend' : 'touchmove'; + self._bind(el, tt, function(e) { + if (e.touches) { + if (e.touches.length < 2) { + var ev = (e.touches.length) ? e.touches[0] : e; + ev.original = e; + fn.call(this, ev); + } + } else if (e.changedTouches) { + var ev = e.changedTouches[0]; + ev.original = e; + fn.call(this, ev); + } //blackberry + }, bubble || false); + } + self._bind(el, name, fn, bubble || false); + if (cap.cantouch && name == "mouseup") self._bind(el, "touchcancel", fn, bubble || false); + } else { + self._bind(el, name, function(e) { + e = e || window.event || false; + if (e) { + if (e.srcElement) e.target = e.srcElement; + } + if (!("pageY" in e)) { + e.pageX = e.clientX + document.documentElement.scrollLeft; + e.pageY = e.clientY + document.documentElement.scrollTop; + } + return ((fn.call(el, e) === false) || bubble === false) ? self.cancelEvent(e) : true; + }); + } + }; + + if (cap.haseventlistener) { // W3C standard model + this._bind = function(el, name, fn, bubble) { // primitive bind + self.events.push({ + e: el, + n: name, + f: fn, + b: bubble, + q: false + }); + el.addEventListener(name, fn, bubble || false); + }; + this.cancelEvent = function(e) { + if (!e) return false; + var e = (e.original) ? e.original : e; + e.preventDefault(); + e.stopPropagation(); + if (e.preventManipulation) e.preventManipulation(); //IE10 + return false; + }; + this.stopPropagation = function(e) { + if (!e) return false; + var e = (e.original) ? e.original : e; + e.stopPropagation(); + return false; + }; + this._unbind = function(el, name, fn, bub) { // primitive unbind + el.removeEventListener(name, fn, bub); + }; + } else { // old IE model + this._bind = function(el, name, fn, bubble) { // primitive bind + self.events.push({ + e: el, + n: name, + f: fn, + b: bubble, + q: false + }); + if (el.attachEvent) { + el.attachEvent("on" + name, fn); + } else { + el["on" + name] = fn; + } + }; + // Thanks to http://www.switchonthecode.com !! + this.cancelEvent = function(e) { + var e = window.event || false; + if (!e) return false; + e.cancelBubble = true; + e.cancel = true; + e.returnValue = false; + return false; + }; + this.stopPropagation = function(e) { + var e = window.event || false; + if (!e) return false; + e.cancelBubble = true; + return false; + }; + this._unbind = function(el, name, fn, bub) { // primitive unbind IE old + if (el.detachEvent) { + el.detachEvent('on' + name, fn); + } else { + el['on' + name] = false; + } + }; + } + + this.unbindAll = function() { + for (var a = 0; a < self.events.length; a++) { + var r = self.events[a]; + (r.q) ? r.e.unbind(r.n, r.f): self._unbind(r.e, r.n, r.f, r.b); + } + }; + + this.showRail = function() { + if ((self.page.maxh != 0) && (self.ispage || self.win.css('display') != 'none')) { + self.visibility = true; + self.rail.visibility = true; + self.rail.css('display', 'block'); + } + return self; + }; + + this.showRailHr = function() { + if (!self.railh) return self; + if ((self.page.maxw != 0) && (self.ispage || self.win.css('display') != 'none')) { + self.railh.visibility = true; + self.railh.css('display', 'block'); + } + return self; + }; + + this.hideRail = function() { + self.visibility = false; + self.rail.visibility = false; + self.rail.css('display', 'none'); + return self; + }; + + this.hideRailHr = function() { + if (!self.railh) return self; + self.railh.visibility = false; + self.railh.css('display', 'none'); + return self; + }; + + this.show = function() { + self.hidden = false; + self.railslocked = false; + return self.showRail().showRailHr(); + }; + + this.hide = function() { + self.hidden = true; + self.railslocked = true; + return self.hideRail().hideRailHr(); + }; + + this.toggle = function() { + return (self.hidden) ? self.show() : self.hide(); + }; + + this.remove = function() { + self.stop(); + if (self.cursortimeout) clearTimeout(self.cursortimeout); + self.doZoomOut(); + self.unbindAll(); + + if (cap.isie9) self.win[0].detachEvent("onpropertychange", self.onAttributeChange); //IE9 DOMAttrModified bug + + if (self.observer !== false) self.observer.disconnect(); + if (self.observerremover !== false) self.observerremover.disconnect(); + if (self.observerbody !== false) self.observerbody.disconnect(); + + self.events = null; + + if (self.cursor) { + self.cursor.remove(); + } + if (self.cursorh) { + self.cursorh.remove(); + } + if (self.rail) { + self.rail.remove(); + } + if (self.railh) { + self.railh.remove(); + } + if (self.zoom) { + self.zoom.remove(); + } + for (var a = 0; a < self.saved.css.length; a++) { + var d = self.saved.css[a]; + d[0].css(d[1], (typeof d[2] == "undefined") ? '' : d[2]); + } + self.saved = false; + self.me.data('__nicescroll', ''); //erase all traces + + // memory leak fixed by GianlucaGuarini - thanks a lot! + // remove the current nicescroll from the $.nicescroll array & normalize array + var lst = $.nicescroll; + lst.each(function(i) { + if (!this) return; + if (this.id === self.id) { + delete lst[i]; + for (var b = ++i; b < lst.length; b++, i++) lst[i] = lst[b]; + lst.length--; + if (lst.length) delete lst[lst.length]; + } + }); + + for (var i in self) { + self[i] = null; + delete self[i]; + } + + self = null; + + }; + + this.scrollstart = function(fn) { + this.onscrollstart = fn; + return self; + }; + this.scrollend = function(fn) { + this.onscrollend = fn; + return self; + }; + this.scrollcancel = function(fn) { + this.onscrollcancel = fn; + return self; + }; + + this.zoomin = function(fn) { + this.onzoomin = fn; + return self; + }; + this.zoomout = function(fn) { + this.onzoomout = fn; + return self; + }; + + this.isScrollable = function(e) { + var dom = (e.target) ? e.target : e; + if (dom.nodeName == 'OPTION') return true; + while (dom && (dom.nodeType == 1) && !(/^BODY|HTML/.test(dom.nodeName))) { + var dd = $(dom); + var ov = dd.css('overflowY') || dd.css('overflowX') || dd.css('overflow') || ''; + if (/scroll|auto/.test(ov)) return (dom.clientHeight != dom.scrollHeight); + dom = (dom.parentNode) ? dom.parentNode : false; + } + return false; + }; + + this.getViewport = function(me) { + var dom = (me && me.parentNode) ? me.parentNode : false; + while (dom && (dom.nodeType == 1) && !(/^BODY|HTML/.test(dom.nodeName))) { + var dd = $(dom); + if (/fixed|absolute/.test(dd.css("position"))) return dd; + var ov = dd.css('overflowY') || dd.css('overflowX') || dd.css('overflow') || ''; + if ((/scroll|auto/.test(ov)) && (dom.clientHeight != dom.scrollHeight)) return dd; + if (dd.getNiceScroll().length > 0) return dd; + dom = (dom.parentNode) ? dom.parentNode : false; + } + return false; //(dom) ? $(dom) : false; + }; + + this.triggerScrollEnd = function() { + if (!self.onscrollend) return; + + var px = self.getScrollLeft(); + var py = self.getScrollTop(); + + var info = { + "type": "scrollend", + "current": { + "x": px, + "y": py + }, + "end": { + "x": px, + "y": py + } + }; + self.onscrollend.call(self, info); + } + + function execScrollWheel(e, hr, chkscroll) { + var px, py; + + if (e.deltaMode == 0) { // PIXEL + px = -Math.floor(e.deltaX * (self.opt.mousescrollstep / (18 * 3))); + py = -Math.floor(e.deltaY * (self.opt.mousescrollstep / (18 * 3))); + } else if (e.deltaMode == 1) { // LINE + px = -Math.floor(e.deltaX * self.opt.mousescrollstep); + py = -Math.floor(e.deltaY * self.opt.mousescrollstep); + } + + if (hr && self.opt.oneaxismousemode && (px == 0) && py) { // classic vertical-only mousewheel + browser with x/y support + px = py; + py = 0; + + if (chkscroll) { + var hrend = (px < 0) ? (self.getScrollLeft() >= self.page.maxw) : (self.getScrollLeft() <= 0); + if (hrend) { // preserve vertical scrolling + py = px; + px = 0; + } + } + + } + + if (px) { + if (self.scrollmom) { + self.scrollmom.stop() + } + self.lastdeltax += px; + self.debounced("mousewheelx", function() { + var dt = self.lastdeltax; + self.lastdeltax = 0; + if (!self.rail.drag) { + self.doScrollLeftBy(dt) + } + }, 15); + } + if (py) { + if (self.opt.nativeparentscrolling && chkscroll && !self.ispage && !self.zoomactive) { + if (py < 0) { + if (self.getScrollTop() >= self.page.maxh) return true; + } else { + if (self.getScrollTop() <= 0) return true; + } + } + if (self.scrollmom) { + self.scrollmom.stop() + } + self.lastdeltay += py; + self.debounced("mousewheely", function() { + var dt = self.lastdeltay; + self.lastdeltay = 0; + if (!self.rail.drag) { + self.doScrollBy(dt) + } + }, 15); + } + + e.stopImmediatePropagation(); + return e.preventDefault(); + }; + + this.onmousewheel = function(e) { + if (self.wheelprevented) return; + if (self.railslocked) { + self.debounced("checkunlock", self.resize, 250); + return true; + } + if (self.rail.drag) return self.cancelEvent(e); + + if (self.opt.oneaxismousemode == "auto" && e.deltaX != 0) self.opt.oneaxismousemode = false; // check two-axis mouse support (not very elegant) + + if (self.opt.oneaxismousemode && e.deltaX == 0) { + if (!self.rail.scrollable) { + if (self.railh && self.railh.scrollable) { + return self.onmousewheelhr(e); + } else { + return true; + } + } + } + + var nw = +(new Date()); + var chk = false; + if (self.opt.preservenativescrolling && ((self.checkarea + 600) < nw)) { + self.nativescrollingarea = self.isScrollable(e); + chk = true; + } + self.checkarea = nw; + if (self.nativescrollingarea) return true; // this isn't my business + var ret = execScrollWheel(e, false, chk); + if (ret) self.checkarea = 0; + return ret; + }; + + this.onmousewheelhr = function(e) { + if (self.wheelprevented) return; + if (self.railslocked || !self.railh.scrollable) return true; + if (self.rail.drag) return self.cancelEvent(e); + + var nw = +(new Date()); + var chk = false; + if (self.opt.preservenativescrolling && ((self.checkarea + 600) < nw)) { + self.nativescrollingarea = self.isScrollable(e); + chk = true; + } + self.checkarea = nw; + if (self.nativescrollingarea) return true; // this isn't my business + if (self.railslocked) return self.cancelEvent(e); + + return execScrollWheel(e, true, chk); + }; + + this.stop = function() { + self.cancelScroll(); + if (self.scrollmon) self.scrollmon.stop(); + self.cursorfreezed = false; + self.scroll.y = Math.round(self.getScrollTop() * (1 / self.scrollratio.y)); + self.noticeCursor(); + return self; + }; + + this.getTransitionSpeed = function(dif) { + var sp = Math.round(self.opt.scrollspeed * 10); + var ex = Math.min(sp, Math.round((dif / 20) * self.opt.scrollspeed)); + return (ex > 20) ? ex : 0; + }; + + if (!self.opt.smoothscroll) { + this.doScrollLeft = function(x, spd) { //direct + var y = self.getScrollTop(); + self.doScrollPos(x, y, spd); + }; + this.doScrollTop = function(y, spd) { //direct + var x = self.getScrollLeft(); + self.doScrollPos(x, y, spd); + }; + this.doScrollPos = function(x, y, spd) { //direct + var nx = (x > self.page.maxw) ? self.page.maxw : x; + if (nx < 0) nx = 0; + var ny = (y > self.page.maxh) ? self.page.maxh : y; + if (ny < 0) ny = 0; + self.synched('scroll', function() { + self.setScrollTop(ny); + self.setScrollLeft(nx); + }); + }; + this.cancelScroll = function() {}; // direct + } else if (self.ishwscroll && cap.hastransition && self.opt.usetransition && !!self.opt.smoothscroll) { + this.prepareTransition = function(dif, istime) { + var ex = (istime) ? ((dif > 20) ? dif : 0) : self.getTransitionSpeed(dif); + var trans = (ex) ? cap.prefixstyle + 'transform ' + ex + 'ms ease-out' : ''; + if (!self.lasttransitionstyle || self.lasttransitionstyle != trans) { + self.lasttransitionstyle = trans; + self.doc.css(cap.transitionstyle, trans); + } + return ex; + }; + + this.doScrollLeft = function(x, spd) { //trans + var y = (self.scrollrunning) ? self.newscrolly : self.getScrollTop(); + self.doScrollPos(x, y, spd); + }; + + this.doScrollTop = function(y, spd) { //trans + var x = (self.scrollrunning) ? self.newscrollx : self.getScrollLeft(); + self.doScrollPos(x, y, spd); + }; + + this.doScrollPos = function(x, y, spd) { //trans + + var py = self.getScrollTop(); + var px = self.getScrollLeft(); + + if (((self.newscrolly - py) * (y - py) < 0) || ((self.newscrollx - px) * (x - px) < 0)) self.cancelScroll(); //inverted movement detection + + if (self.opt.bouncescroll == false) { + if (y < 0) y = 0; + else if (y > self.page.maxh) y = self.page.maxh; + if (x < 0) x = 0; + else if (x > self.page.maxw) x = self.page.maxw; + } + + if (self.scrollrunning && x == self.newscrollx && y == self.newscrolly) return false; + + self.newscrolly = y; + self.newscrollx = x; + + self.newscrollspeed = spd || false; + + if (self.timer) return false; + + self.timer = setTimeout(function() { + + var top = self.getScrollTop(); + var lft = self.getScrollLeft(); + + var dst = {}; + dst.x = x - lft; + dst.y = y - top; + dst.px = lft; + dst.py = top; + + var dd = Math.round(Math.sqrt(Math.pow(dst.x, 2) + Math.pow(dst.y, 2))); + var ms = (self.newscrollspeed && self.newscrollspeed > 1) ? self.newscrollspeed : self.getTransitionSpeed(dd); + if (self.newscrollspeed && self.newscrollspeed <= 1) ms *= self.newscrollspeed; + + self.prepareTransition(ms, true); + + if (self.timerscroll && self.timerscroll.tm) clearInterval(self.timerscroll.tm); + + if (ms > 0) { + + if (!self.scrollrunning && self.onscrollstart) { + var info = { + "type": "scrollstart", + "current": { + "x": lft, + "y": top + }, + "request": { + "x": x, + "y": y + }, + "end": { + "x": self.newscrollx, + "y": self.newscrolly + }, + "speed": ms + }; + self.onscrollstart.call(self, info); + } + + if (cap.transitionend) { + if (!self.scrollendtrapped) { + self.scrollendtrapped = true; + self.bind(self.doc, cap.transitionend, self.onScrollTransitionEnd, false); //I have got to do something usefull!! + } + } else { + if (self.scrollendtrapped) clearTimeout(self.scrollendtrapped); + self.scrollendtrapped = setTimeout(self.onScrollTransitionEnd, ms); // simulate transitionend event + } + + var py = top; + var px = lft; + self.timerscroll = { + bz: new BezierClass(py, self.newscrolly, ms, 0, 0, 0.58, 1), + bh: new BezierClass(px, self.newscrollx, ms, 0, 0, 0.58, 1) + }; + if (!self.cursorfreezed) self.timerscroll.tm = setInterval(function() { + self.showCursor(self.getScrollTop(), self.getScrollLeft()) + }, 60); + + } + + self.synched("doScroll-set", function() { + self.timer = 0; + if (self.scrollendtrapped) self.scrollrunning = true; + self.setScrollTop(self.newscrolly); + self.setScrollLeft(self.newscrollx); + if (!self.scrollendtrapped) self.onScrollTransitionEnd(); + }); + + + }, 50); + + }; + + this.cancelScroll = function() { + if (!self.scrollendtrapped) return true; + var py = self.getScrollTop(); + var px = self.getScrollLeft(); + self.scrollrunning = false; + if (!cap.transitionend) clearTimeout(cap.transitionend); + self.scrollendtrapped = false; + self._unbind(self.doc[0], cap.transitionend, self.onScrollTransitionEnd); + self.prepareTransition(0); + self.setScrollTop(py); // fire event onscroll + if (self.railh) self.setScrollLeft(px); + if (self.timerscroll && self.timerscroll.tm) clearInterval(self.timerscroll.tm); + self.timerscroll = false; + + self.cursorfreezed = false; + + self.showCursor(py, px); + return self; + }; + this.onScrollTransitionEnd = function() { + if (self.scrollendtrapped) self._unbind(self.doc[0], cap.transitionend, self.onScrollTransitionEnd); + self.scrollendtrapped = false; + self.prepareTransition(0); + if (self.timerscroll && self.timerscroll.tm) clearInterval(self.timerscroll.tm); + self.timerscroll = false; + var py = self.getScrollTop(); + var px = self.getScrollLeft(); + self.setScrollTop(py); // fire event onscroll + if (self.railh) self.setScrollLeft(px); // fire event onscroll left + + self.noticeCursor(false, py, px); + + self.cursorfreezed = false; + + if (py < 0) py = 0 + else if (py > self.page.maxh) py = self.page.maxh; + if (px < 0) px = 0 + else if (px > self.page.maxw) px = self.page.maxw; + if ((py != self.newscrolly) || (px != self.newscrollx)) return self.doScrollPos(px, py, self.opt.snapbackspeed); + + if (self.onscrollend && self.scrollrunning) { + self.triggerScrollEnd(); + } + self.scrollrunning = false; + + }; + + } else { + + this.doScrollLeft = function(x, spd) { //no-trans + var y = (self.scrollrunning) ? self.newscrolly : self.getScrollTop(); + self.doScrollPos(x, y, spd); + }; + + this.doScrollTop = function(y, spd) { //no-trans + var x = (self.scrollrunning) ? self.newscrollx : self.getScrollLeft(); + self.doScrollPos(x, y, spd); + }; + + this.doScrollPos = function(x, y, spd) { //no-trans + var y = ((typeof y == "undefined") || (y === false)) ? self.getScrollTop(true) : y; + + if ((self.timer) && (self.newscrolly == y) && (self.newscrollx == x)) return true; + + if (self.timer) clearAnimationFrame(self.timer); + self.timer = 0; + + var py = self.getScrollTop(); + var px = self.getScrollLeft(); + + if (((self.newscrolly - py) * (y - py) < 0) || ((self.newscrollx - px) * (x - px) < 0)) self.cancelScroll(); //inverted movement detection + + self.newscrolly = y; + self.newscrollx = x; + + if (!self.bouncescroll || !self.rail.visibility) { + if (self.newscrolly < 0) { + self.newscrolly = 0; + } else if (self.newscrolly > self.page.maxh) { + self.newscrolly = self.page.maxh; + } + } + if (!self.bouncescroll || !self.railh.visibility) { + if (self.newscrollx < 0) { + self.newscrollx = 0; + } else if (self.newscrollx > self.page.maxw) { + self.newscrollx = self.page.maxw; + } + } + + self.dst = {}; + self.dst.x = x - px; + self.dst.y = y - py; + self.dst.px = px; + self.dst.py = py; + + var dst = Math.round(Math.sqrt(Math.pow(self.dst.x, 2) + Math.pow(self.dst.y, 2))); + + self.dst.ax = self.dst.x / dst; + self.dst.ay = self.dst.y / dst; + + var pa = 0; + var pe = dst; + + if (self.dst.x == 0) { + pa = py; + pe = y; + self.dst.ay = 1; + self.dst.py = 0; + } else if (self.dst.y == 0) { + pa = px; + pe = x; + self.dst.ax = 1; + self.dst.px = 0; + } + + var ms = self.getTransitionSpeed(dst); + if (spd && spd <= 1) ms *= spd; + if (ms > 0) { + self.bzscroll = (self.bzscroll) ? self.bzscroll.update(pe, ms) : new BezierClass(pa, pe, ms, 0, 1, 0, 1); + } else { + self.bzscroll = false; + } + + if (self.timer) return; + + if ((py == self.page.maxh && y >= self.page.maxh) || (px == self.page.maxw && x >= self.page.maxw)) self.checkContentSize(); + + var sync = 1; + + function scrolling() { + if (self.cancelAnimationFrame) return true; + + self.scrollrunning = true; + + sync = 1 - sync; + if (sync) return (self.timer = setAnimationFrame(scrolling) || 1); + + var done = 0; + var sx, sy; + + var sc = sy = self.getScrollTop(); + if (self.dst.ay) { + sc = (self.bzscroll) ? self.dst.py + (self.bzscroll.getNow() * self.dst.ay) : self.newscrolly; + var dr = sc - sy; + if ((dr < 0 && sc < self.newscrolly) || (dr > 0 && sc > self.newscrolly)) sc = self.newscrolly; + self.setScrollTop(sc); + if (sc == self.newscrolly) done = 1; + } else { + done = 1; + } + + var scx = sx = self.getScrollLeft(); + if (self.dst.ax) { + scx = (self.bzscroll) ? self.dst.px + (self.bzscroll.getNow() * self.dst.ax) : self.newscrollx; + var dr = scx - sx; + if ((dr < 0 && scx < self.newscrollx) || (dr > 0 && scx > self.newscrollx)) scx = self.newscrollx; + self.setScrollLeft(scx); + if (scx == self.newscrollx) done += 1; + } else { + done += 1; + } + + if (done == 2) { + self.timer = 0; + self.cursorfreezed = false; + self.bzscroll = false; + self.scrollrunning = false; + if (sc < 0) sc = 0; + else if (sc > self.page.maxh) sc = self.page.maxh; + if (scx < 0) scx = 0; + else if (scx > self.page.maxw) scx = self.page.maxw; + if ((scx != self.newscrollx) || (sc != self.newscrolly)) self.doScrollPos(scx, sc); + else { + if (self.onscrollend) { + self.triggerScrollEnd(); + } + } + } else { + self.timer = setAnimationFrame(scrolling) || 1; + } + }; + self.cancelAnimationFrame = false; + self.timer = 1; + + if (self.onscrollstart && !self.scrollrunning) { + var info = { + "type": "scrollstart", + "current": { + "x": px, + "y": py + }, + "request": { + "x": x, + "y": y + }, + "end": { + "x": self.newscrollx, + "y": self.newscrolly + }, + "speed": ms + }; + self.onscrollstart.call(self, info); + } + + scrolling(); + + if ((py == self.page.maxh && y >= py) || (px == self.page.maxw && x >= px)) self.checkContentSize(); + + self.noticeCursor(); + }; + + this.cancelScroll = function() { + if (self.timer) clearAnimationFrame(self.timer); + self.timer = 0; + self.bzscroll = false; + self.scrollrunning = false; + return self; + }; + + } + + this.doScrollBy = function(stp, relative) { + var ny = 0; + if (relative) { + ny = Math.floor((self.scroll.y - stp) * self.scrollratio.y) + } else { + var sy = (self.timer) ? self.newscrolly : self.getScrollTop(true); + ny = sy - stp; + } + if (self.bouncescroll) { + var haf = Math.round(self.view.h / 2); + if (ny < -haf) ny = -haf + else if (ny > (self.page.maxh + haf)) ny = (self.page.maxh + haf); + } + self.cursorfreezed = false; + + var py = self.getScrollTop(true); + if (ny < 0 && py <= 0) return self.noticeCursor(); + else if (ny > self.page.maxh && py >= self.page.maxh) { + self.checkContentSize(); + return self.noticeCursor(); + } + + self.doScrollTop(ny); + }; + + this.doScrollLeftBy = function(stp, relative) { + var nx = 0; + if (relative) { + nx = Math.floor((self.scroll.x - stp) * self.scrollratio.x) + } else { + var sx = (self.timer) ? self.newscrollx : self.getScrollLeft(true); + nx = sx - stp; + } + if (self.bouncescroll) { + var haf = Math.round(self.view.w / 2); + if (nx < -haf) nx = -haf; + else if (nx > (self.page.maxw + haf)) nx = (self.page.maxw + haf); + } + self.cursorfreezed = false; + + var px = self.getScrollLeft(true); + if (nx < 0 && px <= 0) return self.noticeCursor(); + else if (nx > self.page.maxw && px >= self.page.maxw) return self.noticeCursor(); + + self.doScrollLeft(nx); + }; + + this.doScrollTo = function(pos, relative) { + var ny = (relative) ? Math.round(pos * self.scrollratio.y) : pos; + if (ny < 0) ny = 0; + else if (ny > self.page.maxh) ny = self.page.maxh; + self.cursorfreezed = false; + self.doScrollTop(pos); + }; + + this.checkContentSize = function() { + var pg = self.getContentSize(); + if ((pg.h != self.page.h) || (pg.w != self.page.w)) self.resize(false, pg); + }; + + self.onscroll = function(e) { + if (self.rail.drag) return; + if (!self.cursorfreezed) { + self.synched('scroll', function() { + self.scroll.y = Math.round(self.getScrollTop() * (1 / self.scrollratio.y)); + if (self.railh) self.scroll.x = Math.round(self.getScrollLeft() * (1 / self.scrollratio.x)); + self.noticeCursor(); + }); + } + }; + self.bind(self.docscroll, "scroll", self.onscroll); + + this.doZoomIn = function(e) { + if (self.zoomactive) return; + self.zoomactive = true; + + self.zoomrestore = { + style: {} + }; + var lst = ['position', 'top', 'left', 'zIndex', 'backgroundColor', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight']; + var win = self.win[0].style; + for (var a in lst) { + var pp = lst[a]; + self.zoomrestore.style[pp] = (typeof win[pp] != "undefined") ? win[pp] : ''; + } + + self.zoomrestore.style.width = self.win.css('width'); + self.zoomrestore.style.height = self.win.css('height'); + + self.zoomrestore.padding = { + w: self.win.outerWidth() - self.win.width(), + h: self.win.outerHeight() - self.win.height() + }; + + if (cap.isios4) { + self.zoomrestore.scrollTop = $(window).scrollTop(); + $(window).scrollTop(0); + } + + self.win.css({ + "position": (cap.isios4) ? "absolute" : "fixed", + "top": 0, + "left": 0, + "z-index": globalmaxzindex + 100, + "margin": "0px" + }); + var bkg = self.win.css("backgroundColor"); + if (bkg == "" || /transparent|rgba\(0, 0, 0, 0\)|rgba\(0,0,0,0\)/.test(bkg)) self.win.css("backgroundColor", "#fff"); + self.rail.css({ + "z-index": globalmaxzindex + 101 + }); + self.zoom.css({ + "z-index": globalmaxzindex + 102 + }); + self.zoom.css('backgroundPosition', '0px -18px'); + self.resizeZoom(); + + if (self.onzoomin) self.onzoomin.call(self); + + return self.cancelEvent(e); + }; + + this.doZoomOut = function(e) { + if (!self.zoomactive) return; + self.zoomactive = false; + + self.win.css("margin", ""); + self.win.css(self.zoomrestore.style); + + if (cap.isios4) { + $(window).scrollTop(self.zoomrestore.scrollTop); + } + + self.rail.css({ + "z-index": self.zindex + }); + self.zoom.css({ + "z-index": self.zindex + }); + self.zoomrestore = false; + self.zoom.css('backgroundPosition', '0px 0px'); + self.onResize(); + + if (self.onzoomout) self.onzoomout.call(self); + + return self.cancelEvent(e); + }; + + this.doZoom = function(e) { + return (self.zoomactive) ? self.doZoomOut(e) : self.doZoomIn(e); + }; + + this.resizeZoom = function() { + if (!self.zoomactive) return; + + var py = self.getScrollTop(); //preserve scrolling position + self.win.css({ + width: $(window).width() - self.zoomrestore.padding.w + "px", + height: $(window).height() - self.zoomrestore.padding.h + "px" + }); + self.onResize(); + + self.setScrollTop(Math.min(self.page.maxh, py)); + }; + + this.init(); + + $.nicescroll.push(this); + + }; + + // Inspired by the work of Kin Blas + // http://webpro.host.adobe.com/people/jblas/momentum/includes/jquery.momentum.0.7.js + + + var ScrollMomentumClass2D = function(nc) { + var self = this; + this.nc = nc; + + this.lastx = 0; + this.lasty = 0; + this.speedx = 0; + this.speedy = 0; + this.lasttime = 0; + this.steptime = 0; + this.snapx = false; + this.snapy = false; + this.demulx = 0; + this.demuly = 0; + + this.lastscrollx = -1; + this.lastscrolly = -1; + + this.chkx = 0; + this.chky = 0; + + this.timer = 0; + + this.time = function() { + return +new Date(); //beautifull hack + }; + + this.reset = function(px, py) { + self.stop(); + var now = self.time(); + self.steptime = 0; + self.lasttime = now; + self.speedx = 0; + self.speedy = 0; + self.lastx = px; + self.lasty = py; + self.lastscrollx = -1; + self.lastscrolly = -1; + }; + + this.update = function(px, py) { + var now = self.time(); + self.steptime = now - self.lasttime; + self.lasttime = now; + var dy = py - self.lasty; + var dx = px - self.lastx; + var sy = self.nc.getScrollTop(); + var sx = self.nc.getScrollLeft(); + var newy = sy + dy; + var newx = sx + dx; + self.snapx = (newx < 0) || (newx > self.nc.page.maxw); + self.snapy = (newy < 0) || (newy > self.nc.page.maxh); + self.speedx = dx; + self.speedy = dy; + self.lastx = px; + self.lasty = py; + }; + + this.stop = function() { + self.nc.unsynched("domomentum2d"); + if (self.timer) clearTimeout(self.timer); + self.timer = 0; + self.lastscrollx = -1; + self.lastscrolly = -1; + }; + + this.doSnapy = function(nx, ny) { + var snap = false; + + if (ny < 0) { + ny = 0; + snap = true; + } else if (ny > self.nc.page.maxh) { + ny = self.nc.page.maxh; + snap = true; + } + + if (nx < 0) { + nx = 0; + snap = true; + } else if (nx > self.nc.page.maxw) { + nx = self.nc.page.maxw; + snap = true; + } + + (snap) ? self.nc.doScrollPos(nx, ny, self.nc.opt.snapbackspeed): self.nc.triggerScrollEnd(); + }; + + this.doMomentum = function(gp) { + var t = self.time(); + var l = (gp) ? t + gp : self.lasttime; + + var sl = self.nc.getScrollLeft(); + var st = self.nc.getScrollTop(); + + var pageh = self.nc.page.maxh; + var pagew = self.nc.page.maxw; + + self.speedx = (pagew > 0) ? Math.min(60, self.speedx) : 0; + self.speedy = (pageh > 0) ? Math.min(60, self.speedy) : 0; + + var chk = l && (t - l) <= 60; + + if ((st < 0) || (st > pageh) || (sl < 0) || (sl > pagew)) chk = false; + + var sy = (self.speedy && chk) ? self.speedy : false; + var sx = (self.speedx && chk) ? self.speedx : false; + + if (sy || sx) { + var tm = Math.max(16, self.steptime); //timeout granularity + + if (tm > 50) { // do smooth + var xm = tm / 50; + self.speedx *= xm; + self.speedy *= xm; + tm = 50; + } + + self.demulxy = 0; + + self.lastscrollx = self.nc.getScrollLeft(); + self.chkx = self.lastscrollx; + self.lastscrolly = self.nc.getScrollTop(); + self.chky = self.lastscrolly; + + var nx = self.lastscrollx; + var ny = self.lastscrolly; + + var onscroll = function() { + var df = ((self.time() - t) > 600) ? 0.04 : 0.02; + + if (self.speedx) { + nx = Math.floor(self.lastscrollx - (self.speedx * (1 - self.demulxy))); + self.lastscrollx = nx; + if ((nx < 0) || (nx > pagew)) df = 0.10; + } + + if (self.speedy) { + ny = Math.floor(self.lastscrolly - (self.speedy * (1 - self.demulxy))); + self.lastscrolly = ny; + if ((ny < 0) || (ny > pageh)) df = 0.10; + } + + self.demulxy = Math.min(1, self.demulxy + df); + + self.nc.synched("domomentum2d", function() { + + if (self.speedx) { + var scx = self.nc.getScrollLeft(); + if (scx != self.chkx) self.stop(); + self.chkx = nx; + self.nc.setScrollLeft(nx); + } + + if (self.speedy) { + var scy = self.nc.getScrollTop(); + if (scy != self.chky) self.stop(); + self.chky = ny; + self.nc.setScrollTop(ny); + } + + if (!self.timer) { + self.nc.hideCursor(); + self.doSnapy(nx, ny); + } + + }); + + if (self.demulxy < 1) { + self.timer = setTimeout(onscroll, tm); + } else { + self.stop(); + self.nc.hideCursor(); + self.doSnapy(nx, ny); + } + }; + + onscroll(); + + } else { + self.doSnapy(self.nc.getScrollLeft(), self.nc.getScrollTop()); + } + + } + + }; + + + // override jQuery scrollTop + + var _scrollTop = jQuery.fn.scrollTop; // preserve original function + + jQuery.cssHooks["pageYOffset"] = { + get: function(elem, computed, extra) { + var nice = $.data(elem, '__nicescroll') || false; + return (nice && nice.ishwscroll) ? nice.getScrollTop() : _scrollTop.call(elem); + }, + set: function(elem, value) { + var nice = $.data(elem, '__nicescroll') || false; + (nice && nice.ishwscroll) ? nice.setScrollTop(parseInt(value)): _scrollTop.call(elem, value); + return this; + } + }; + + /* + $.fx.step["scrollTop"] = function(fx){ + $.cssHooks["scrollTop"].set( fx.elem, fx.now + fx.unit ); + }; +*/ + + jQuery.fn.scrollTop = function(value) { + if (typeof value == "undefined") { + var nice = (this[0]) ? $.data(this[0], '__nicescroll') || false : false; + return (nice && nice.ishwscroll) ? nice.getScrollTop() : _scrollTop.call(this); + } else { + return this.each(function() { + var nice = $.data(this, '__nicescroll') || false; + (nice && nice.ishwscroll) ? nice.setScrollTop(parseInt(value)): _scrollTop.call($(this), value); + }); + } + }; + + // override jQuery scrollLeft + + var _scrollLeft = jQuery.fn.scrollLeft; // preserve original function + + $.cssHooks.pageXOffset = { + get: function(elem, computed, extra) { + var nice = $.data(elem, '__nicescroll') || false; + return (nice && nice.ishwscroll) ? nice.getScrollLeft() : _scrollLeft.call(elem); + }, + set: function(elem, value) { + var nice = $.data(elem, '__nicescroll') || false; + (nice && nice.ishwscroll) ? nice.setScrollLeft(parseInt(value)): _scrollLeft.call(elem, value); + return this; + } + }; + + /* + $.fx.step["scrollLeft"] = function(fx){ + $.cssHooks["scrollLeft"].set( fx.elem, fx.now + fx.unit ); + }; +*/ + + jQuery.fn.scrollLeft = function(value) { + if (typeof value == "undefined") { + var nice = (this[0]) ? $.data(this[0], '__nicescroll') || false : false; + return (nice && nice.ishwscroll) ? nice.getScrollLeft() : _scrollLeft.call(this); + } else { + return this.each(function() { + var nice = $.data(this, '__nicescroll') || false; + (nice && nice.ishwscroll) ? nice.setScrollLeft(parseInt(value)): _scrollLeft.call($(this), value); + }); + } + }; + + var NiceScrollArray = function(doms) { + var self = this; + this.length = 0; + this.name = "nicescrollarray"; + + this.each = function(fn) { + for (var a = 0, i = 0; a < self.length; a++) fn.call(self[a], i++); + return self; + }; + + this.push = function(nice) { + self[self.length] = nice; + self.length++; + }; + + this.eq = function(idx) { + return self[idx]; + }; + + if (doms) { + for (var a = 0; a < doms.length; a++) { + var nice = $.data(doms[a], '__nicescroll') || false; + if (nice) { + this[this.length] = nice; + this.length++; + } + }; + } + + return this; + }; + + function mplex(el, lst, fn) { + for (var a = 0; a < lst.length; a++) fn(el, lst[a]); + }; + mplex( + NiceScrollArray.prototype, ['show', 'hide', 'toggle', 'onResize', 'resize', 'remove', 'stop', 'doScrollPos'], + function(e, n) { + e[n] = function() { + var args = arguments; + return this.each(function() { + this[n].apply(this, args); + }); + }; + } + ); + + jQuery.fn.getNiceScroll = function(index) { + if (typeof index == "undefined") { + return new NiceScrollArray(this); + } else { + var nice = this[index] && $.data(this[index], '__nicescroll') || false; + return nice; + } + }; + + jQuery.extend(jQuery.expr[':'], { + nicescroll: function(a) { + return ($.data(a, '__nicescroll')) ? true : false; + } + }); + + $.fn.niceScroll = function(wrapper, opt) { + if (typeof opt == "undefined") { + if ((typeof wrapper == "object") && !("jquery" in wrapper)) { + opt = wrapper; + wrapper = false; + } + } + opt = $.extend({},opt); // cloning + var ret = new NiceScrollArray(); + if (typeof opt == "undefined") opt = {}; + + if (wrapper || false) { + opt.doc = $(wrapper); + opt.win = $(this); + } + var docundef = !("doc" in opt); + if (!docundef && !("win" in opt)) opt.win = $(this); + + this.each(function() { + var nice = $(this).data('__nicescroll') || false; + if (!nice) { + opt.doc = (docundef) ? $(this) : opt.doc; + nice = new NiceScrollClass(opt, $(this)); + $(this).data('__nicescroll', nice); + } + ret.push(nice); + }); + return (ret.length == 1) ? ret[0] : ret; + }; + + window.NiceScroll = { + getjQuery: function() { + return jQuery + } + }; + + if (!$.nicescroll) { + $.nicescroll = new NiceScrollArray(); + $.nicescroll.options = _globaloptions; + } + +})); \ No newline at end of file diff --git a/vendor/assets/javascripts/jquery.nicescroll.min.js b/vendor/assets/javascripts/jquery.nicescroll.min.js deleted file mode 100644 index 5440b6a0da0..00000000000 --- a/vendor/assets/javascripts/jquery.nicescroll.min.js +++ /dev/null @@ -1,118 +0,0 @@ -/* jquery.nicescroll 3.6.0 InuYaksa*2014 MIT http://nicescroll.areaaperta.com */(function(f){"function"===typeof define&&define.amd?define(["jquery"],f):f(jQuery)})(function(f){var y=!1,D=!1,N=0,O=2E3,x=0,H=["webkit","ms","moz","o"],s=window.requestAnimationFrame||!1,t=window.cancelAnimationFrame||!1;if(!s)for(var P in H){var E=H[P];s||(s=window[E+"RequestAnimationFrame"]);t||(t=window[E+"CancelAnimationFrame"]||window[E+"CancelRequestAnimationFrame"])}var v=window.MutationObserver||window.WebKitMutationObserver||!1,I={zindex:"auto",cursoropacitymin:0,cursoropacitymax:1,cursorcolor:"#424242", -cursorwidth:"5px",cursorborder:"1px solid #fff",cursorborderradius:"5px",scrollspeed:60,mousescrollstep:24,touchbehavior:!1,hwacceleration:!0,usetransition:!0,boxzoom:!1,dblclickzoom:!0,gesturezoom:!0,grabcursorenabled:!0,autohidemode:!0,background:"",iframeautoresize:!0,cursorminheight:32,preservenativescrolling:!0,railoffset:!1,railhoffset:!1,bouncescroll:!0,spacebarenabled:!0,railpadding:{top:0,right:0,left:0,bottom:0},disableoutline:!0,horizrailenabled:!0,railalign:"right",railvalign:"bottom", -enabletranslate3d:!0,enablemousewheel:!0,enablekeyboard:!0,smoothscroll:!0,sensitiverail:!0,enablemouselockapi:!0,cursorfixedheight:!1,directionlockdeadzone:6,hidecursordelay:400,nativeparentscrolling:!0,enablescrollonselection:!0,overflowx:!0,overflowy:!0,cursordragspeed:.3,rtlmode:"auto",cursordragontouch:!1,oneaxismousemode:"auto",scriptpath:function(){var f=document.getElementsByTagName("script"),f=f[f.length-1].src.split("?")[0];return 0d?a.getScrollLeft()>=a.page.maxw:0>=a.getScrollLeft())&&(e=d,d=0));d&&(a.scrollmom&&a.scrollmom.stop(),a.lastdeltax+=d,a.debounced("mousewheelx",function(){var b=a.lastdeltax;a.lastdeltax=0;a.rail.drag||a.doScrollLeftBy(b)},15));if(e){if(a.opt.nativeparentscrolling&&c&&!a.ispage&&!a.zoomactive)if(0>e){if(a.getScrollTop()>=a.page.maxh)return!0}else if(0>=a.getScrollTop())return!0;a.scrollmom&&a.scrollmom.stop();a.lastdeltay+=e;a.debounced("mousewheely", -function(){var b=a.lastdeltay;a.lastdeltay=0;a.rail.drag||a.doScrollBy(b)},15)}b.stopImmediatePropagation();return b.preventDefault()}var a=this;this.version="3.6.0";this.name="nicescroll";this.me=c;this.opt={doc:f("body"),win:!1};f.extend(this.opt,I);this.opt.snapbackspeed=80;if(k)for(var G in a.opt)"undefined"!=typeof k[G]&&(a.opt[G]=k[G]);this.iddoc=(this.doc=a.opt.doc)&&this.doc[0]?this.doc[0].id||"":"";this.ispage=/^BODY|HTML/.test(a.opt.win?a.opt.win[0].nodeName:this.doc[0].nodeName);this.haswrapper= -!1!==a.opt.win;this.win=a.opt.win||(this.ispage?f(window):this.doc);this.docscroll=this.ispage&&!this.haswrapper?f(window):this.win;this.body=f("body");this.iframe=this.isfixed=this.viewport=!1;this.isiframe="IFRAME"==this.doc[0].nodeName&&"IFRAME"==this.win[0].nodeName;this.istextarea="TEXTAREA"==this.win[0].nodeName;this.forcescreen=!1;this.canshowonmouseevent="scroll"!=a.opt.autohidemode;this.page=this.view=this.onzoomout=this.onzoomin=this.onscrollcancel=this.onscrollend=this.onscrollstart=this.onclick= -this.ongesturezoom=this.onkeypress=this.onmousewheel=this.onmousemove=this.onmouseup=this.onmousedown=!1;this.scroll={x:0,y:0};this.scrollratio={x:0,y:0};this.cursorheight=20;this.scrollvaluemax=0;this.isrtlmode="auto"==this.opt.rtlmode?"rtl"==(this.win[0]==window?this.body:this.win).css("direction"):!0===this.opt.rtlmode;this.observerbody=this.observerremover=this.observer=this.scrollmom=this.scrollrunning=!1;do this.id="ascrail"+O++;while(document.getElementById(this.id));this.hasmousefocus=this.hasfocus= -this.zoomactive=this.zoom=this.selectiondrag=this.cursorfreezed=this.cursor=this.rail=!1;this.visibility=!0;this.hidden=this.locked=this.railslocked=!1;this.cursoractive=!0;this.wheelprevented=!1;this.overflowx=a.opt.overflowx;this.overflowy=a.opt.overflowy;this.nativescrollingarea=!1;this.checkarea=0;this.events=[];this.saved={};this.delaylist={};this.synclist={};this.lastdeltay=this.lastdeltax=0;this.detected=Q();var e=f.extend({},this.detected);this.ishwscroll=(this.canhwscroll=e.hastransform&& -a.opt.hwacceleration)&&a.haswrapper;this.hasreversehr=this.isrtlmode&&!e.iswebkit;this.istouchcapable=!1;!e.cantouch||e.isios||e.isandroid||!e.iswebkit&&!e.ismozilla||(this.istouchcapable=!0,e.cantouch=!1);a.opt.enablemouselockapi||(e.hasmousecapture=!1,e.haspointerlock=!1);this.debounced=function(b,g,c){var d=a.delaylist[b];a.delaylist[b]=g;d||setTimeout(function(){var g=a.delaylist[b];a.delaylist[b]=!1;g.call(a)},c)};var r=!1;this.synched=function(b,g){a.synclist[b]=g;(function(){r||(s(function(){r= -!1;for(var b in a.synclist){var g=a.synclist[b];g&&g.call(a);a.synclist[b]=!1}}),r=!0)})();return b};this.unsynched=function(b){a.synclist[b]&&(a.synclist[b]=!1)};this.css=function(b,g){for(var c in g)a.saved.css.push([b,c,b.css(c)]),b.css(c,g[c])};this.scrollTop=function(b){return"undefined"==typeof b?a.getScrollTop():a.setScrollTop(b)};this.scrollLeft=function(b){return"undefined"==typeof b?a.getScrollLeft():a.setScrollLeft(b)};var A=function(a,g,c,d,e,f,h){this.st=a;this.ed=g;this.spd=c;this.p1= -d||0;this.p2=e||1;this.p3=f||0;this.p4=h||1;this.ts=(new Date).getTime();this.df=this.ed-this.st};A.prototype={B2:function(a){return 3*a*a*(1-a)},B3:function(a){return 3*a*(1-a)*(1-a)},B4:function(a){return(1-a)*(1-a)*(1-a)},getNow:function(){var a=1-((new Date).getTime()-this.ts)/this.spd,g=this.B2(a)+this.B3(a)+this.B4(a);return 0>a?this.ed:this.st+Math.round(this.df*g)},update:function(a,g){this.st=this.getNow();this.ed=a;this.spd=g;this.ts=(new Date).getTime();this.df=this.ed-this.st;return this}}; -if(this.ishwscroll){this.doc.translate={x:0,y:0,tx:"0px",ty:"0px"};e.hastranslate3d&&e.isios&&this.doc.css("-webkit-backface-visibility","hidden");this.getScrollTop=function(b){if(!b){if(b=h())return 16==b.length?-b[13]:-b[5];if(a.timerscroll&&a.timerscroll.bz)return a.timerscroll.bz.getNow()}return a.doc.translate.y};this.getScrollLeft=function(b){if(!b){if(b=h())return 16==b.length?-b[12]:-b[4];if(a.timerscroll&&a.timerscroll.bh)return a.timerscroll.bh.getNow()}return a.doc.translate.x};this.notifyScrollEvent= -function(a){var g=document.createEvent("UIEvents");g.initUIEvent("scroll",!1,!0,window,1);g.niceevent=!0;a.dispatchEvent(g)};var K=this.isrtlmode?1:-1;e.hastranslate3d&&a.opt.enabletranslate3d?(this.setScrollTop=function(b,g){a.doc.translate.y=b;a.doc.translate.ty=-1*b+"px";a.doc.css(e.trstyle,"translate3d("+a.doc.translate.tx+","+a.doc.translate.ty+",0px)");g||a.notifyScrollEvent(a.win[0])},this.setScrollLeft=function(b,g){a.doc.translate.x=b;a.doc.translate.tx=b*K+"px";a.doc.css(e.trstyle,"translate3d("+ -a.doc.translate.tx+","+a.doc.translate.ty+",0px)");g||a.notifyScrollEvent(a.win[0])}):(this.setScrollTop=function(b,g){a.doc.translate.y=b;a.doc.translate.ty=-1*b+"px";a.doc.css(e.trstyle,"translate("+a.doc.translate.tx+","+a.doc.translate.ty+")");g||a.notifyScrollEvent(a.win[0])},this.setScrollLeft=function(b,g){a.doc.translate.x=b;a.doc.translate.tx=b*K+"px";a.doc.css(e.trstyle,"translate("+a.doc.translate.tx+","+a.doc.translate.ty+")");g||a.notifyScrollEvent(a.win[0])})}else this.getScrollTop= -function(){return a.docscroll.scrollTop()},this.setScrollTop=function(b){return a.docscroll.scrollTop(b)},this.getScrollLeft=function(){return a.detected.ismozilla&&a.isrtlmode?Math.abs(a.docscroll.scrollLeft()):a.docscroll.scrollLeft()},this.setScrollLeft=function(b){return a.docscroll.scrollLeft(a.detected.ismozilla&&a.isrtlmode?-b:b)};this.getTarget=function(a){return a?a.target?a.target:a.srcElement?a.srcElement:!1:!1};this.hasParent=function(a,g){if(!a)return!1;for(var c=a.target||a.srcElement|| -a||!1;c&&c.id!=g;)c=c.parentNode||!1;return!1!==c};var w={thin:1,medium:3,thick:5};this.getDocumentScrollOffset=function(){return{top:window.pageYOffset||document.documentElement.scrollTop,left:window.pageXOffset||document.documentElement.scrollLeft}};this.getOffset=function(){if(a.isfixed){var b=a.win.offset(),g=a.getDocumentScrollOffset();b.top-=g.top;b.left-=g.left;return b}b=a.win.offset();if(!a.viewport)return b;g=a.viewport.offset();return{top:b.top-g.top,left:b.left-g.left}};this.updateScrollBar= -function(b){if(a.ishwscroll)a.rail.css({height:a.win.innerHeight()-(a.opt.railpadding.top+a.opt.railpadding.bottom)}),a.railh&&a.railh.css({width:a.win.innerWidth()-(a.opt.railpadding.left+a.opt.railpadding.right)});else{var g=a.getOffset(),c=g.top,e=g.left-(a.opt.railpadding.left+a.opt.railpadding.right),c=c+d(a.win,"border-top-width",!0),e=e+(a.rail.align?a.win.outerWidth()-d(a.win,"border-right-width")-a.rail.width:d(a.win,"border-left-width")),f=a.opt.railoffset;f&&(f.top&&(c+=f.top),a.rail.align&& -f.left&&(e+=f.left));a.railslocked||a.rail.css({top:c,left:e,height:(b?b.h:a.win.innerHeight())-(a.opt.railpadding.top+a.opt.railpadding.bottom)});a.zoom&&a.zoom.css({top:c+1,left:1==a.rail.align?e-20:e+a.rail.width+4});if(a.railh&&!a.railslocked){c=g.top;e=g.left;if(f=a.opt.railhoffset)f.top&&(c+=f.top),f.left&&(e+=f.left);b=a.railh.align?c+d(a.win,"border-top-width",!0)+a.win.innerHeight()-a.railh.height:c+d(a.win,"border-top-width",!0);e+=d(a.win,"border-left-width");a.railh.css({top:b-(a.opt.railpadding.top+ -a.opt.railpadding.bottom),left:e,width:a.railh.width})}}};this.doRailClick=function(b,g,c){var e;a.railslocked||(a.cancelEvent(b),g?(g=c?a.doScrollLeft:a.doScrollTop,e=c?(b.pageX-a.railh.offset().left-a.cursorwidth/2)*a.scrollratio.x:(b.pageY-a.rail.offset().top-a.cursorheight/2)*a.scrollratio.y,g(e)):(g=c?a.doScrollLeftBy:a.doScrollBy,e=c?a.scroll.x:a.scroll.y,b=c?b.pageX-a.railh.offset().left:b.pageY-a.rail.offset().top,c=c?a.view.w:a.view.h,g(e>=b?c:-c)))};a.hasanimationframe=s;a.hascancelanimationframe= -t;a.hasanimationframe?a.hascancelanimationframe||(t=function(){a.cancelAnimationFrame=!0}):(s=function(a){return setTimeout(a,15-Math.floor(+new Date/1E3)%16)},t=clearInterval);this.init=function(){a.saved.css=[];if(e.isie7mobile||e.isoperamini)return!0;e.hasmstouch&&a.css(a.ispage?f("html"):a.win,{"-ms-touch-action":"none"});a.zindex="auto";a.zindex=a.ispage||"auto"!=a.opt.zindex?a.opt.zindex:m()||"auto";!a.ispage&&"auto"!=a.zindex&&a.zindex>x&&(x=a.zindex);a.isie&&0==a.zindex&&"auto"==a.opt.zindex&& -(a.zindex="auto");if(!a.ispage||!e.cantouch&&!e.isieold&&!e.isie9mobile){var b=a.docscroll;a.ispage&&(b=a.haswrapper?a.win:a.doc);e.isie9mobile||a.css(b,{"overflow-y":"hidden"});a.ispage&&e.isie7&&("BODY"==a.doc[0].nodeName?a.css(f("html"),{"overflow-y":"hidden"}):"HTML"==a.doc[0].nodeName&&a.css(f("body"),{"overflow-y":"hidden"}));!e.isios||a.ispage||a.haswrapper||a.css(f("body"),{"-webkit-overflow-scrolling":"touch"});var g=f(document.createElement("div"));g.css({position:"relative",top:0,"float":"right", -width:a.opt.cursorwidth,height:"0px","background-color":a.opt.cursorcolor,border:a.opt.cursorborder,"background-clip":"padding-box","-webkit-border-radius":a.opt.cursorborderradius,"-moz-border-radius":a.opt.cursorborderradius,"border-radius":a.opt.cursorborderradius});g.hborder=parseFloat(g.outerHeight()-g.innerHeight());g.addClass("nicescroll-cursors");a.cursor=g;var c=f(document.createElement("div"));c.attr("id",a.id);c.addClass("nicescroll-rails nicescroll-rails-vr");var d,h,k=["left","right", -"top","bottom"],J;for(J in k)h=k[J],(d=a.opt.railpadding[h])?c.css("padding-"+h,d+"px"):a.opt.railpadding[h]=0;c.append(g);c.width=Math.max(parseFloat(a.opt.cursorwidth),g.outerWidth());c.css({width:c.width+"px",zIndex:a.zindex,background:a.opt.background,cursor:"default"});c.visibility=!0;c.scrollable=!0;c.align="left"==a.opt.railalign?0:1;a.rail=c;g=a.rail.drag=!1;!a.opt.boxzoom||a.ispage||e.isieold||(g=document.createElement("div"),a.bind(g,"click",a.doZoom),a.bind(g,"mouseenter",function(){a.zoom.css("opacity", -a.opt.cursoropacitymax)}),a.bind(g,"mouseleave",function(){a.zoom.css("opacity",a.opt.cursoropacitymin)}),a.zoom=f(g),a.zoom.css({cursor:"pointer","z-index":a.zindex,backgroundImage:"url("+a.opt.scriptpath+"zoomico.png)",height:18,width:18,backgroundPosition:"0px 0px"}),a.opt.dblclickzoom&&a.bind(a.win,"dblclick",a.doZoom),e.cantouch&&a.opt.gesturezoom&&(a.ongesturezoom=function(b){1.5b.scale&&a.doZoomOut(b);return a.cancelEvent(b)},a.bind(a.win,"gestureend",a.ongesturezoom))); -a.railh=!1;var l;a.opt.horizrailenabled&&(a.css(b,{"overflow-x":"hidden"}),g=f(document.createElement("div")),g.css({position:"absolute",top:0,height:a.opt.cursorwidth,width:"0px","background-color":a.opt.cursorcolor,border:a.opt.cursorborder,"background-clip":"padding-box","-webkit-border-radius":a.opt.cursorborderradius,"-moz-border-radius":a.opt.cursorborderradius,"border-radius":a.opt.cursorborderradius}),e.isieold&&g.css({overflow:"hidden"}),g.wborder=parseFloat(g.outerWidth()-g.innerWidth()), -g.addClass("nicescroll-cursors"),a.cursorh=g,l=f(document.createElement("div")),l.attr("id",a.id+"-hr"),l.addClass("nicescroll-rails nicescroll-rails-hr"),l.height=Math.max(parseFloat(a.opt.cursorwidth),g.outerHeight()),l.css({height:l.height+"px",zIndex:a.zindex,background:a.opt.background}),l.append(g),l.visibility=!0,l.scrollable=!0,l.align="top"==a.opt.railvalign?0:1,a.railh=l,a.railh.drag=!1);a.ispage?(c.css({position:"fixed",top:"0px",height:"100%"}),c.align?c.css({right:"0px"}):c.css({left:"0px"}), -a.body.append(c),a.railh&&(l.css({position:"fixed",left:"0px",width:"100%"}),l.align?l.css({bottom:"0px"}):l.css({top:"0px"}),a.body.append(l))):(a.ishwscroll?("static"==a.win.css("position")&&a.css(a.win,{position:"relative"}),b="HTML"==a.win[0].nodeName?a.body:a.win,f(b).scrollTop(0).scrollLeft(0),a.zoom&&(a.zoom.css({position:"absolute",top:1,right:0,"margin-right":c.width+4}),b.append(a.zoom)),c.css({position:"absolute",top:0}),c.align?c.css({right:0}):c.css({left:0}),b.append(c),l&&(l.css({position:"absolute", -left:0,bottom:0}),l.align?l.css({bottom:0}):l.css({top:0}),b.append(l))):(a.isfixed="fixed"==a.win.css("position"),b=a.isfixed?"fixed":"absolute",a.isfixed||(a.viewport=a.getViewport(a.win[0])),a.viewport&&(a.body=a.viewport,0==/fixed|absolute/.test(a.viewport.css("position"))&&a.css(a.viewport,{position:"relative"})),c.css({position:b}),a.zoom&&a.zoom.css({position:b}),a.updateScrollBar(),a.body.append(c),a.zoom&&a.body.append(a.zoom),a.railh&&(l.css({position:b}),a.body.append(l))),e.isios&&a.css(a.win, -{"-webkit-tap-highlight-color":"rgba(0,0,0,0)","-webkit-touch-callout":"none"}),e.isie&&a.opt.disableoutline&&a.win.attr("hideFocus","true"),e.iswebkit&&a.opt.disableoutline&&a.win.css({outline:"none"}));!1===a.opt.autohidemode?(a.autohidedom=!1,a.rail.css({opacity:a.opt.cursoropacitymax}),a.railh&&a.railh.css({opacity:a.opt.cursoropacitymax})):!0===a.opt.autohidemode||"leave"===a.opt.autohidemode?(a.autohidedom=f().add(a.rail),e.isie8&&(a.autohidedom=a.autohidedom.add(a.cursor)),a.railh&&(a.autohidedom= -a.autohidedom.add(a.railh)),a.railh&&e.isie8&&(a.autohidedom=a.autohidedom.add(a.cursorh))):"scroll"==a.opt.autohidemode?(a.autohidedom=f().add(a.rail),a.railh&&(a.autohidedom=a.autohidedom.add(a.railh))):"cursor"==a.opt.autohidemode?(a.autohidedom=f().add(a.cursor),a.railh&&(a.autohidedom=a.autohidedom.add(a.cursorh))):"hidden"==a.opt.autohidemode&&(a.autohidedom=!1,a.hide(),a.railslocked=!1);if(e.isie9mobile)a.scrollmom=new L(a),a.onmangotouch=function(){var b=a.getScrollTop(),c=a.getScrollLeft(); -if(b==a.scrollmom.lastscrolly&&c==a.scrollmom.lastscrollx)return!0;var g=b-a.mangotouch.sy,e=c-a.mangotouch.sx;if(0!=Math.round(Math.sqrt(Math.pow(e,2)+Math.pow(g,2)))){var d=0>g?-1:1,f=0>e?-1:1,q=+new Date;a.mangotouch.lazy&&clearTimeout(a.mangotouch.lazy);80k?k=Math.round(k/2):k>a.page.maxh&&(k=a.page.maxh+Math.round((k-a.page.maxh)/2)):(0>k&&(q=k=0),k>a.page.maxh&&(k=a.page.maxh,q=0));var l;a.railh&&a.railh.scrollable&&(l=a.isrtlmode?u-a.rail.drag.sl:a.rail.drag.sl-u,a.ishwscroll&&a.opt.bouncescroll?0>l?l=Math.round(l/2):l>a.page.maxw&&(l=a.page.maxw+Math.round((l-a.page.maxw)/2)):(0>l&&(h=l=0),l>a.page.maxw&&(l=a.page.maxw,h=0)));g=!1;if(a.rail.drag.dl)g= -!0,"v"==a.rail.drag.dl?l=a.rail.drag.sl:"h"==a.rail.drag.dl&&(k=a.rail.drag.st);else{d=Math.abs(d);var u=Math.abs(u),z=a.opt.directionlockdeadzone;if("v"==a.rail.drag.ck){if(d>z&&u<=.3*d)return a.rail.drag=!1,!0;u>z&&(a.rail.drag.dl="f",f("body").scrollTop(f("body").scrollTop()))}else if("h"==a.rail.drag.ck){if(u>z&&d<=.3*u)return a.rail.drag=!1,!0;d>z&&(a.rail.drag.dl="f",f("body").scrollLeft(f("body").scrollLeft()))}}a.synched("touchmove",function(){a.rail.drag&&2==a.rail.drag.pt&&(a.prepareTransition&& -a.prepareTransition(0),a.rail.scrollable&&a.setScrollTop(k),a.scrollmom.update(h,q),a.railh&&a.railh.scrollable?(a.setScrollLeft(l),a.showCursor(k,l)):a.showCursor(k),e.isie10&&document.selection.clear())});e.ischrome&&a.istouchcapable&&(g=!1);if(g)return a.cancelEvent(b)}else if(1==a.rail.drag.pt)return a.onmousemove(b)}}a.onmousedown=function(b,c){if(!a.rail.drag||1==a.rail.drag.pt){if(a.railslocked)return a.cancelEvent(b);a.cancelScroll();a.rail.drag={x:b.clientX,y:b.clientY,sx:a.scroll.x,sy:a.scroll.y, -pt:1,hr:!!c};var g=a.getTarget(b);!a.ispage&&e.hasmousecapture&&g.setCapture();a.isiframe&&!e.hasmousecapture&&(a.saved.csspointerevents=a.doc.css("pointer-events"),a.css(a.doc,{"pointer-events":"none"}));a.hasmoving=!1;return a.cancelEvent(b)}};a.onmouseup=function(b){if(a.rail.drag){if(1!=a.rail.drag.pt)return!0;e.hasmousecapture&&document.releaseCapture();a.isiframe&&!e.hasmousecapture&&a.doc.css("pointer-events",a.saved.csspointerevents);a.rail.drag=!1;a.hasmoving&&a.triggerScrollEnd();return a.cancelEvent(b)}}; -a.onmousemove=function(b){if(a.rail.drag&&1==a.rail.drag.pt){if(e.ischrome&&0==b.which)return a.onmouseup(b);a.cursorfreezed=!0;a.hasmoving=!0;if(a.rail.drag.hr){a.scroll.x=a.rail.drag.sx+(b.clientX-a.rail.drag.x);0>a.scroll.x&&(a.scroll.x=0);var c=a.scrollvaluemaxw;a.scroll.x>c&&(a.scroll.x=c)}else a.scroll.y=a.rail.drag.sy+(b.clientY-a.rail.drag.y),0>a.scroll.y&&(a.scroll.y=0),c=a.scrollvaluemax,a.scroll.y>c&&(a.scroll.y=c);a.synched("mousemove",function(){a.rail.drag&&1==a.rail.drag.pt&&(a.showCursor(), -a.rail.drag.hr?a.hasreversehr?a.doScrollLeft(a.scrollvaluemaxw-Math.round(a.scroll.x*a.scrollratio.x),a.opt.cursordragspeed):a.doScrollLeft(Math.round(a.scroll.x*a.scrollratio.x),a.opt.cursordragspeed):a.doScrollTop(Math.round(a.scroll.y*a.scrollratio.y),a.opt.cursordragspeed))});return a.cancelEvent(b)}};if(e.cantouch||a.opt.touchbehavior)a.onpreventclick=function(b){if(a.preventclick)return a.preventclick.tg.onclick=a.preventclick.click,a.preventclick=!1,a.cancelEvent(b)},a.bind(a.win,"mousedown", -a.ontouchstart),a.onclick=e.isios?!1:function(b){return a.lastmouseup?(a.lastmouseup=!1,a.cancelEvent(b)):!0},a.opt.grabcursorenabled&&e.cursorgrabvalue&&(a.css(a.ispage?a.doc:a.win,{cursor:e.cursorgrabvalue}),a.css(a.rail,{cursor:e.cursorgrabvalue}));else{var p=function(b){if(a.selectiondrag){if(b){var c=a.win.outerHeight();b=b.pageY-a.selectiondrag.top;0=c&&(b-=c);a.selectiondrag.df=b}0!=a.selectiondrag.df&&(a.doScrollBy(2*-Math.floor(a.selectiondrag.df/6)),a.debounced("doselectionscroll", -function(){p()},50))}};a.hasTextSelected="getSelection"in document?function(){return 0a.page.maxh?a.doScrollTop(a.page.maxh):(a.scroll.y=Math.round(a.getScrollTop()*(1/a.scrollratio.y)),a.scroll.x=Math.round(a.getScrollLeft()*(1/a.scrollratio.x)),a.cursoractive&&a.noticeCursor());a.scroll.y&&0==a.getScrollTop()&&a.doScrollTo(Math.floor(a.scroll.y*a.scrollratio.y));return a};this.resize=a.onResize;this.lazyResize=function(b){b=isNaN(b)?30:b;a.debounced("resize",a.resize,b);return a};this.jqbind=function(b, -c,d){a.events.push({e:b,n:c,f:d,q:!0});f(b).bind(c,d)};this.bind=function(b,c,d,f){var h="jquery"in b?b[0]:b;"mousewheel"==c?window.addEventListener||"onwheel"in document?a._bind(h,"wheel",d,f||!1):(b="undefined"!=typeof document.onmousewheel?"mousewheel":"DOMMouseScroll",n(h,b,d,f||!1),"DOMMouseScroll"==b&&n(h,"MozMousePixelScroll",d,f||!1)):h.addEventListener?(e.cantouch&&/mouseup|mousedown|mousemove/.test(c)&&a._bind(h,"mousedown"==c?"touchstart":"mouseup"==c?"touchend":"touchmove",function(a){if(a.touches){if(2> -a.touches.length){var b=a.touches.length?a.touches[0]:a;b.original=a;d.call(this,b)}}else a.changedTouches&&(b=a.changedTouches[0],b.original=a,d.call(this,b))},f||!1),a._bind(h,c,d,f||!1),e.cantouch&&"mouseup"==c&&a._bind(h,"touchcancel",d,f||!1)):a._bind(h,c,function(b){(b=b||window.event||!1)&&b.srcElement&&(b.target=b.srcElement);"pageY"in b||(b.pageX=b.clientX+document.documentElement.scrollLeft,b.pageY=b.clientY+document.documentElement.scrollTop);return!1===d.call(h,b)||!1===f?a.cancelEvent(b): -!0})};e.haseventlistener?(this._bind=function(b,c,d,e){a.events.push({e:b,n:c,f:d,b:e,q:!1});b.addEventListener(c,d,e||!1)},this.cancelEvent=function(a){if(!a)return!1;a=a.original?a.original:a;a.preventDefault();a.stopPropagation();a.preventManipulation&&a.preventManipulation();return!1},this.stopPropagation=function(a){if(!a)return!1;a=a.original?a.original:a;a.stopPropagation();return!1},this._unbind=function(a,c,d,e){a.removeEventListener(c,d,e)}):(this._bind=function(b,c,d,e){a.events.push({e:b, -n:c,f:d,b:e,q:!1});b.attachEvent?b.attachEvent("on"+c,d):b["on"+c]=d},this.cancelEvent=function(a){a=window.event||!1;if(!a)return!1;a.cancelBubble=!0;a.cancel=!0;return a.returnValue=!1},this.stopPropagation=function(a){a=window.event||!1;if(!a)return!1;a.cancelBubble=!0;return!1},this._unbind=function(a,c,d,e){a.detachEvent?a.detachEvent("on"+c,d):a["on"+c]=!1});this.unbindAll=function(){for(var b=0;b(a.newscrolly- -f)*(c-f)||0>(a.newscrollx-h)*(b-h))&&a.cancelScroll();0==a.opt.bouncescroll&&(0>c?c=0:c>a.page.maxh&&(c=a.page.maxh),0>b?b=0:b>a.page.maxw&&(b=a.page.maxw));if(a.scrollrunning&&b==a.newscrollx&&c==a.newscrolly)return!1;a.newscrolly=c;a.newscrollx=b;a.newscrollspeed=d||!1;if(a.timer)return!1;a.timer=setTimeout(function(){var d=a.getScrollTop(),f=a.getScrollLeft(),h,k;h=b-f;k=c-d;h=Math.round(Math.sqrt(Math.pow(h,2)+Math.pow(k,2)));h=a.newscrollspeed&&1=a.newscrollspeed&&(h*=a.newscrollspeed);a.prepareTransition(h,!0);a.timerscroll&&a.timerscroll.tm&&clearInterval(a.timerscroll.tm);0b?b=0:b>a.page.maxh&&(b=a.page.maxh);0>c?c=0:c>a.page.maxw&&(c=a.page.maxw);if(b!=a.newscrolly||c!=a.newscrollx)return a.doScrollPos(c,b,a.opt.snapbackspeed);a.onscrollend&&a.scrollrunning&&a.triggerScrollEnd();a.scrollrunning=!1}):(this.doScrollLeft= -function(b,c){var d=a.scrollrunning?a.newscrolly:a.getScrollTop();a.doScrollPos(b,d,c)},this.doScrollTop=function(b,c){var d=a.scrollrunning?a.newscrollx:a.getScrollLeft();a.doScrollPos(d,b,c)},this.doScrollPos=function(b,c,d){function e(){if(a.cancelAnimationFrame)return!0;a.scrollrunning=!0;if(n=1-n)return a.timer=s(e)||1;var b=0,c,d,g=d=a.getScrollTop();if(a.dst.ay){g=a.bzscroll?a.dst.py+a.bzscroll.getNow()*a.dst.ay:a.newscrolly;c=g-d;if(0>c&&ga.newscrolly)g=a.newscrolly; -a.setScrollTop(g);g==a.newscrolly&&(b=1)}else b=1;d=c=a.getScrollLeft();if(a.dst.ax){d=a.bzscroll?a.dst.px+a.bzscroll.getNow()*a.dst.ax:a.newscrollx;c=d-c;if(0>c&&da.newscrollx)d=a.newscrollx;a.setScrollLeft(d);d==a.newscrollx&&(b+=1)}else b+=1;2==b?(a.timer=0,a.cursorfreezed=!1,a.bzscroll=!1,a.scrollrunning=!1,0>g?g=0:g>a.page.maxh&&(g=a.page.maxh),0>d?d=0:d>a.page.maxw&&(d=a.page.maxw),d!=a.newscrollx||g!=a.newscrolly?a.doScrollPos(d,g):a.onscrollend&&a.triggerScrollEnd()): -a.timer=s(e)||1}c="undefined"==typeof c||!1===c?a.getScrollTop(!0):c;if(a.timer&&a.newscrolly==c&&a.newscrollx==b)return!0;a.timer&&t(a.timer);a.timer=0;var f=a.getScrollTop(),h=a.getScrollLeft();(0>(a.newscrolly-f)*(c-f)||0>(a.newscrollx-h)*(b-h))&&a.cancelScroll();a.newscrolly=c;a.newscrollx=b;a.bouncescroll&&a.rail.visibility||(0>a.newscrolly?a.newscrolly=0:a.newscrolly>a.page.maxh&&(a.newscrolly=a.page.maxh));a.bouncescroll&&a.railh.visibility||(0>a.newscrollx?a.newscrollx=0:a.newscrollx>a.page.maxw&& -(a.newscrollx=a.page.maxw));a.dst={};a.dst.x=b-h;a.dst.y=c-f;a.dst.px=h;a.dst.py=f;var k=Math.round(Math.sqrt(Math.pow(a.dst.x,2)+Math.pow(a.dst.y,2)));a.dst.ax=a.dst.x/k;a.dst.ay=a.dst.y/k;var l=0,m=k;0==a.dst.x?(l=f,m=c,a.dst.ay=1,a.dst.py=0):0==a.dst.y&&(l=h,m=b,a.dst.ax=1,a.dst.px=0);k=a.getTransitionSpeed(k);d&&1>=d&&(k*=d);a.bzscroll=0=a.page.maxh||h==a.page.maxw&&b>=a.page.maxw)&&a.checkContentSize(); -var n=1;a.cancelAnimationFrame=!1;a.timer=1;a.onscrollstart&&!a.scrollrunning&&a.onscrollstart.call(a,{type:"scrollstart",current:{x:h,y:f},request:{x:b,y:c},end:{x:a.newscrollx,y:a.newscrolly},speed:k});e();(f==a.page.maxh&&c>=f||h==a.page.maxw&&b>=h)&&a.checkContentSize();a.noticeCursor()}},this.cancelScroll=function(){a.timer&&t(a.timer);a.timer=0;a.bzscroll=!1;a.scrollrunning=!1;return a}):(this.doScrollLeft=function(b,c){var d=a.getScrollTop();a.doScrollPos(b,d,c)},this.doScrollTop=function(b, -c){var d=a.getScrollLeft();a.doScrollPos(d,b,c)},this.doScrollPos=function(b,c,d){var e=b>a.page.maxw?a.page.maxw:b;0>e&&(e=0);var f=c>a.page.maxh?a.page.maxh:c;0>f&&(f=0);a.synched("scroll",function(){a.setScrollTop(f);a.setScrollLeft(e)})},this.cancelScroll=function(){});this.doScrollBy=function(b,c){var d=0,d=c?Math.floor((a.scroll.y-b)*a.scrollratio.y):(a.timer?a.newscrolly:a.getScrollTop(!0))-b;if(a.bouncescroll){var e=Math.round(a.view.h/2);d<-e?d=-e:d>a.page.maxh+e&&(d=a.page.maxh+e)}a.cursorfreezed= -!1;e=a.getScrollTop(!0);if(0>d&&0>=e)return a.noticeCursor();if(d>a.page.maxh&&e>=a.page.maxh)return a.checkContentSize(),a.noticeCursor();a.doScrollTop(d)};this.doScrollLeftBy=function(b,c){var d=0,d=c?Math.floor((a.scroll.x-b)*a.scrollratio.x):(a.timer?a.newscrollx:a.getScrollLeft(!0))-b;if(a.bouncescroll){var e=Math.round(a.view.w/2);d<-e?d=-e:d>a.page.maxw+e&&(d=a.page.maxw+e)}a.cursorfreezed=!1;e=a.getScrollLeft(!0);if(0>d&&0>=e||d>a.page.maxw&&e>=a.page.maxw)return a.noticeCursor();a.doScrollLeft(d)}; -this.doScrollTo=function(b,c){c&&Math.round(b*a.scrollratio.y);a.cursorfreezed=!1;a.doScrollTop(b)};this.checkContentSize=function(){var b=a.getContentSize();b.h==a.page.h&&b.w==a.page.w||a.resize(!1,b)};a.onscroll=function(b){a.rail.drag||a.cursorfreezed||a.synched("scroll",function(){a.scroll.y=Math.round(a.getScrollTop()*(1/a.scrollratio.y));a.railh&&(a.scroll.x=Math.round(a.getScrollLeft()*(1/a.scrollratio.x)));a.noticeCursor()})};a.bind(a.docscroll,"scroll",a.onscroll);this.doZoomIn=function(b){if(!a.zoomactive){a.zoomactive= -!0;a.zoomrestore={style:{}};var c="position top left zIndex backgroundColor marginTop marginBottom marginLeft marginRight".split(" "),d=a.win[0].style,h;for(h in c){var k=c[h];a.zoomrestore.style[k]="undefined"!=typeof d[k]?d[k]:""}a.zoomrestore.style.width=a.win.css("width");a.zoomrestore.style.height=a.win.css("height");a.zoomrestore.padding={w:a.win.outerWidth()-a.win.width(),h:a.win.outerHeight()-a.win.height()};e.isios4&&(a.zoomrestore.scrollTop=f(window).scrollTop(),f(window).scrollTop(0)); -a.win.css({position:e.isios4?"absolute":"fixed",top:0,left:0,"z-index":x+100,margin:"0px"});c=a.win.css("backgroundColor");(""==c||/transparent|rgba\(0, 0, 0, 0\)|rgba\(0,0,0,0\)/.test(c))&&a.win.css("backgroundColor","#fff");a.rail.css({"z-index":x+101});a.zoom.css({"z-index":x+102});a.zoom.css("backgroundPosition","0px -18px");a.resizeZoom();a.onzoomin&&a.onzoomin.call(a);return a.cancelEvent(b)}};this.doZoomOut=function(b){if(a.zoomactive)return a.zoomactive=!1,a.win.css("margin",""),a.win.css(a.zoomrestore.style), -e.isios4&&f(window).scrollTop(a.zoomrestore.scrollTop),a.rail.css({"z-index":a.zindex}),a.zoom.css({"z-index":a.zindex}),a.zoomrestore=!1,a.zoom.css("backgroundPosition","0px 0px"),a.onResize(),a.onzoomout&&a.onzoomout.call(a),a.cancelEvent(b)};this.doZoom=function(b){return a.zoomactive?a.doZoomOut(b):a.doZoomIn(b)};this.resizeZoom=function(){if(a.zoomactive){var b=a.getScrollTop();a.win.css({width:f(window).width()-a.zoomrestore.padding.w+"px",height:f(window).height()-a.zoomrestore.padding.h+"px"}); -a.onResize();a.setScrollTop(Math.min(a.page.maxh,b))}};this.init();f.nicescroll.push(this)},L=function(f){var c=this;this.nc=f;this.steptime=this.lasttime=this.speedy=this.speedx=this.lasty=this.lastx=0;this.snapy=this.snapx=!1;this.demuly=this.demulx=0;this.lastscrolly=this.lastscrollx=-1;this.timer=this.chky=this.chkx=0;this.time=function(){return+new Date};this.reset=function(f,k){c.stop();var d=c.time();c.steptime=0;c.lasttime=d;c.speedx=0;c.speedy=0;c.lastx=f;c.lasty=k;c.lastscrollx=-1;c.lastscrolly= --1};this.update=function(f,k){var d=c.time();c.steptime=d-c.lasttime;c.lasttime=d;var d=k-c.lasty,n=f-c.lastx,p=c.nc.getScrollTop(),a=c.nc.getScrollLeft(),p=p+d,a=a+n;c.snapx=0>a||a>c.nc.page.maxw;c.snapy=0>p||p>c.nc.page.maxh;c.speedx=n;c.speedy=d;c.lastx=f;c.lasty=k};this.stop=function(){c.nc.unsynched("domomentum2d");c.timer&&clearTimeout(c.timer);c.timer=0;c.lastscrollx=-1;c.lastscrolly=-1};this.doSnapy=function(f,k){var d=!1;0>k?(k=0,d=!0):k>c.nc.page.maxh&&(k=c.nc.page.maxh,d=!0);0>f?(f=0,d= -!0):f>c.nc.page.maxw&&(f=c.nc.page.maxw,d=!0);d?c.nc.doScrollPos(f,k,c.nc.opt.snapbackspeed):c.nc.triggerScrollEnd()};this.doMomentum=function(f){var k=c.time(),d=f?k+f:c.lasttime;f=c.nc.getScrollLeft();var n=c.nc.getScrollTop(),p=c.nc.page.maxh,a=c.nc.page.maxw;c.speedx=0=k-d;if(0>n||n>p||0>f||f>a)d=!1;f=c.speedx&&d?c.speedx:!1;if(c.speedy&&d&&c.speedy||f){var s=Math.max(16,c.steptime);50e||e>a)&&(d=.1);c.speedy&&(r=Math.floor(c.lastscrolly-c.speedy*(1-c.demulxy)),c.lastscrolly=r,0>r||r>p)&&(d=.1);c.demulxy=Math.min(1,c.demulxy+d);c.nc.synched("domomentum2d",function(){c.speedx&&(c.nc.getScrollLeft()!= -c.chkx&&c.stop(),c.chkx=e,c.nc.setScrollLeft(e));c.speedy&&(c.nc.getScrollTop()!=c.chky&&c.stop(),c.chky=r,c.nc.setScrollTop(r));c.timer||(c.nc.hideCursor(),c.doSnapy(e,r))});1>c.demulxy?c.timer=setTimeout(t,s):(c.stop(),c.nc.hideCursor(),c.doSnapy(e,r))};t()}else c.doSnapy(c.nc.getScrollLeft(),c.nc.getScrollTop())}},w=f.fn.scrollTop;f.cssHooks.pageYOffset={get:function(k,c,h){return(c=f.data(k,"__nicescroll")||!1)&&c.ishwscroll?c.getScrollTop():w.call(k)},set:function(k,c){var h=f.data(k,"__nicescroll")|| -!1;h&&h.ishwscroll?h.setScrollTop(parseInt(c)):w.call(k,c);return this}};f.fn.scrollTop=function(k){if("undefined"==typeof k){var c=this[0]?f.data(this[0],"__nicescroll")||!1:!1;return c&&c.ishwscroll?c.getScrollTop():w.call(this)}return this.each(function(){var c=f.data(this,"__nicescroll")||!1;c&&c.ishwscroll?c.setScrollTop(parseInt(k)):w.call(f(this),k)})};var B=f.fn.scrollLeft;f.cssHooks.pageXOffset={get:function(k,c,h){return(c=f.data(k,"__nicescroll")||!1)&&c.ishwscroll?c.getScrollLeft():B.call(k)}, -set:function(k,c){var h=f.data(k,"__nicescroll")||!1;h&&h.ishwscroll?h.setScrollLeft(parseInt(c)):B.call(k,c);return this}};f.fn.scrollLeft=function(k){if("undefined"==typeof k){var c=this[0]?f.data(this[0],"__nicescroll")||!1:!1;return c&&c.ishwscroll?c.getScrollLeft():B.call(this)}return this.each(function(){var c=f.data(this,"__nicescroll")||!1;c&&c.ishwscroll?c.setScrollLeft(parseInt(k)):B.call(f(this),k)})};var C=function(k){var c=this;this.length=0;this.name="nicescrollarray";this.each=function(d){for(var f= -0,h=0;f Date: Wed, 20 Jan 2016 01:19:53 -0200 Subject: Don't vendor minified fuzzaldrin-plus This version was compiled to use in the browser through Browserify. More info or how to compile a new version: https://gitlab.com/dbalexandre/fuzzaldrin-plus-browserify --- app/assets/javascripts/application.js.coffee | 2 +- vendor/assets/javascripts/fuzzaldrin-plus.js | 1161 ++++++++++++++++++++++ vendor/assets/javascripts/fuzzaldrin-plus.min.js | 1 - 3 files changed, 1162 insertions(+), 2 deletions(-) create mode 100644 vendor/assets/javascripts/fuzzaldrin-plus.js delete mode 100644 vendor/assets/javascripts/fuzzaldrin-plus.min.js diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 9f9b94d1a4a..eb18d32b25c 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -40,7 +40,7 @@ #= require shortcuts_network #= require jquery.nicescroll #= require_tree . -#= require fuzzaldrin-plus.min +#= require fuzzaldrin-plus window.slugify = (text) -> text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase() diff --git a/vendor/assets/javascripts/fuzzaldrin-plus.js b/vendor/assets/javascripts/fuzzaldrin-plus.js new file mode 100644 index 00000000000..1985e3f8f6c --- /dev/null +++ b/vendor/assets/javascripts/fuzzaldrin-plus.js @@ -0,0 +1,1161 @@ +/*! + * fuzzaldrin-plus.js - 0.3.1 + * https://github.com/jeancroy/fuzzaldrin-plus + * + * Copyright 2016 - Jean Christophe Roy + * Released under the MIT license + * https://github.com/jeancroy/fuzzaldrin-plus/raw/master/LICENSE.md + */ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 ? maxInners : candidates.length; + bAllowErrors = !!allowErrors; + bKey = key != null; + prepQuery = scorer.prepQuery(query); + if (!legacy) { + for (_i = 0, _len = candidates.length; _i < _len; _i++) { + candidate = candidates[_i]; + string = bKey ? candidate[key] : candidate; + if (!string) { + continue; + } + score = scorer.score(string, query, prepQuery, bAllowErrors); + if (score > 0) { + scoredCandidates.push({ + candidate: candidate, + score: score + }); + if (!--spotLeft) { + break; + } + } + } + } else { + queryHasSlashes = prepQuery.depth > 0; + coreQuery = prepQuery.core; + for (_j = 0, _len1 = candidates.length; _j < _len1; _j++) { + candidate = candidates[_j]; + string = key != null ? candidate[key] : candidate; + if (!string) { + continue; + } + score = legacy_scorer.score(string, coreQuery, queryHasSlashes); + if (!queryHasSlashes) { + score = legacy_scorer.basenameScore(string, coreQuery, score); + } + if (score > 0) { + scoredCandidates.push({ + candidate: candidate, + score: score + }); + } + } + } + scoredCandidates.sort(sortCandidates); + candidates = scoredCandidates.map(pluckCandidates); + if (maxResults != null) { + candidates = candidates.slice(0, maxResults); + } + return candidates; + }; + +}).call(this); + +},{"./legacy":4,"./scorer":6,"path":7}],3:[function(require,module,exports){ +(function() { + var PathSeparator, filter, legacy_scorer, matcher, prepQueryCache, scorer; + + scorer = require('./scorer'); + + legacy_scorer = require('./legacy'); + + filter = require('./filter'); + + matcher = require('./matcher'); + + PathSeparator = require('path').sep; + + prepQueryCache = null; + + module.exports = { + filter: function(candidates, query, options) { + if (!((query != null ? query.length : void 0) && (candidates != null ? candidates.length : void 0))) { + return []; + } + return filter(candidates, query, options); + }, + prepQuery: function(query) { + return scorer.prepQuery(query); + }, + score: function(string, query, prepQuery, _arg) { + var allowErrors, coreQuery, legacy, queryHasSlashes, score, _ref; + _ref = _arg != null ? _arg : {}, allowErrors = _ref.allowErrors, legacy = _ref.legacy; + if (!((string != null ? string.length : void 0) && (query != null ? query.length : void 0))) { + return 0; + } + if (prepQuery == null) { + prepQuery = prepQueryCache && prepQueryCache.query === query ? prepQueryCache : (prepQueryCache = scorer.prepQuery(query)); + } + if (!legacy) { + score = scorer.score(string, query, prepQuery, !!allowErrors); + } else { + queryHasSlashes = prepQuery.depth > 0; + coreQuery = prepQuery.core; + score = legacy_scorer.score(string, coreQuery, queryHasSlashes); + if (!queryHasSlashes) { + score = legacy_scorer.basenameScore(string, coreQuery, score); + } + } + return score; + }, + match: function(string, query, prepQuery, _arg) { + var allowErrors, baseMatches, matches, query_lw, string_lw, _i, _ref, _results; + allowErrors = (_arg != null ? _arg : {}).allowErrors; + if (!string) { + return []; + } + if (!query) { + return []; + } + if (string === query) { + return (function() { + _results = []; + for (var _i = 0, _ref = string.length; 0 <= _ref ? _i < _ref : _i > _ref; 0 <= _ref ? _i++ : _i--){ _results.push(_i); } + return _results; + }).apply(this); + } + if (prepQuery == null) { + prepQuery = prepQueryCache && prepQueryCache.query === query ? prepQueryCache : (prepQueryCache = scorer.prepQuery(query)); + } + if (!(allowErrors || scorer.isMatch(string, prepQuery.core_lw, prepQuery.core_up))) { + return []; + } + string_lw = string.toLowerCase(); + query_lw = prepQuery.query_lw; + matches = matcher.match(string, string_lw, prepQuery); + if (matches.length === 0) { + return matches; + } + if (string.indexOf(PathSeparator) > -1) { + baseMatches = matcher.basenameMatch(string, string_lw, prepQuery); + matches = matcher.mergeMatches(matches, baseMatches); + } + return matches; + } + }; + +}).call(this); + +},{"./filter":2,"./legacy":4,"./matcher":5,"./scorer":6,"path":7}],4:[function(require,module,exports){ +(function() { + var PathSeparator, queryIsLastPathSegment; + + PathSeparator = require('path').sep; + + exports.basenameScore = function(string, query, score) { + var base, depth, index, lastCharacter, segmentCount, slashCount; + index = string.length - 1; + while (string[index] === PathSeparator) { + index--; + } + slashCount = 0; + lastCharacter = index; + base = null; + while (index >= 0) { + if (string[index] === PathSeparator) { + slashCount++; + if (base == null) { + base = string.substring(index + 1, lastCharacter + 1); + } + } else if (index === 0) { + if (lastCharacter < string.length - 1) { + if (base == null) { + base = string.substring(0, lastCharacter + 1); + } + } else { + if (base == null) { + base = string; + } + } + } + index--; + } + if (base === string) { + score *= 2; + } else if (base) { + score += exports.score(base, query); + } + segmentCount = slashCount + 1; + depth = Math.max(1, 10 - segmentCount); + score *= depth * 0.01; + return score; + }; + + exports.score = function(string, query) { + var character, characterScore, indexInQuery, indexInString, lowerCaseIndex, minIndex, queryLength, queryScore, stringLength, totalCharacterScore, upperCaseIndex, _ref; + if (string === query) { + return 1; + } + if (queryIsLastPathSegment(string, query)) { + return 1; + } + totalCharacterScore = 0; + queryLength = query.length; + stringLength = string.length; + indexInQuery = 0; + indexInString = 0; + while (indexInQuery < queryLength) { + character = query[indexInQuery++]; + lowerCaseIndex = string.indexOf(character.toLowerCase()); + upperCaseIndex = string.indexOf(character.toUpperCase()); + minIndex = Math.min(lowerCaseIndex, upperCaseIndex); + if (minIndex === -1) { + minIndex = Math.max(lowerCaseIndex, upperCaseIndex); + } + indexInString = minIndex; + if (indexInString === -1) { + return 0; + } + characterScore = 0.1; + if (string[indexInString] === character) { + characterScore += 0.1; + } + if (indexInString === 0 || string[indexInString - 1] === PathSeparator) { + characterScore += 0.8; + } else if ((_ref = string[indexInString - 1]) === '-' || _ref === '_' || _ref === ' ') { + characterScore += 0.7; + } + string = string.substring(indexInString + 1, stringLength); + totalCharacterScore += characterScore; + } + queryScore = totalCharacterScore / queryLength; + return ((queryScore * (queryLength / stringLength)) + queryScore) / 2; + }; + + queryIsLastPathSegment = function(string, query) { + if (string[string.length - query.length - 1] === PathSeparator) { + return string.lastIndexOf(query) === string.length - query.length; + } + }; + + exports.match = function(string, query, stringOffset) { + var character, indexInQuery, indexInString, lowerCaseIndex, matches, minIndex, queryLength, stringLength, upperCaseIndex, _i, _ref, _results; + if (stringOffset == null) { + stringOffset = 0; + } + if (string === query) { + return (function() { + _results = []; + for (var _i = stringOffset, _ref = stringOffset + string.length; stringOffset <= _ref ? _i < _ref : _i > _ref; stringOffset <= _ref ? _i++ : _i--){ _results.push(_i); } + return _results; + }).apply(this); + } + queryLength = query.length; + stringLength = string.length; + indexInQuery = 0; + indexInString = 0; + matches = []; + while (indexInQuery < queryLength) { + character = query[indexInQuery++]; + lowerCaseIndex = string.indexOf(character.toLowerCase()); + upperCaseIndex = string.indexOf(character.toUpperCase()); + minIndex = Math.min(lowerCaseIndex, upperCaseIndex); + if (minIndex === -1) { + minIndex = Math.max(lowerCaseIndex, upperCaseIndex); + } + indexInString = minIndex; + if (indexInString === -1) { + return []; + } + matches.push(stringOffset + indexInString); + stringOffset += indexInString + 1; + string = string.substring(indexInString + 1, stringLength); + } + return matches; + }; + +}).call(this); + +},{"path":7}],5:[function(require,module,exports){ +(function() { + var PathSeparator, scorer; + + PathSeparator = require('path').sep; + + scorer = require('./scorer'); + + exports.basenameMatch = function(subject, subject_lw, prepQuery) { + var basePos, depth, end; + end = subject.length - 1; + while (subject[end] === PathSeparator) { + end--; + } + basePos = subject.lastIndexOf(PathSeparator, end); + if (basePos === -1) { + return []; + } + depth = prepQuery.depth; + while (depth-- > 0) { + basePos = subject.lastIndexOf(PathSeparator, basePos - 1); + if (basePos === -1) { + return []; + } + } + basePos++; + end++; + return exports.match(subject.slice(basePos, end), subject_lw.slice(basePos, end), prepQuery, basePos); + }; + + exports.mergeMatches = function(a, b) { + var ai, bj, i, j, m, n, out; + m = a.length; + n = b.length; + if (n === 0) { + return a.slice(); + } + if (m === 0) { + return b.slice(); + } + i = -1; + j = 0; + bj = b[j]; + out = []; + while (++i < m) { + ai = a[i]; + while (bj <= ai && ++j < n) { + if (bj < ai) { + out.push(bj); + } + bj = b[j]; + } + out.push(ai); + } + while (j < n) { + out.push(b[j++]); + } + return out; + }; + + exports.match = function(subject, subject_lw, prepQuery, offset) { + var DIAGONAL, LEFT, STOP, UP, acro_score, align, backtrack, csc_diag, csc_row, csc_score, i, j, m, matches, move, n, pos, query, query_lw, score, score_diag, score_row, score_up, si_lw, start, trace; + if (offset == null) { + offset = 0; + } + query = prepQuery.query; + query_lw = prepQuery.query_lw; + m = subject.length; + n = query.length; + acro_score = scorer.scoreAcronyms(subject, subject_lw, query, query_lw).score; + score_row = new Array(n); + csc_row = new Array(n); + STOP = 0; + UP = 1; + LEFT = 2; + DIAGONAL = 3; + trace = new Array(m * n); + pos = -1; + j = -1; + while (++j < n) { + score_row[j] = 0; + csc_row[j] = 0; + } + i = -1; + while (++i < m) { + score = 0; + score_up = 0; + csc_diag = 0; + si_lw = subject_lw[i]; + j = -1; + while (++j < n) { + csc_score = 0; + align = 0; + score_diag = score_up; + if (query_lw[j] === si_lw) { + start = scorer.isWordStart(i, subject, subject_lw); + csc_score = csc_diag > 0 ? csc_diag : scorer.scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start); + align = score_diag + scorer.scoreCharacter(i, j, start, acro_score, csc_score); + } + score_up = score_row[j]; + csc_diag = csc_row[j]; + if (score > score_up) { + move = LEFT; + } else { + score = score_up; + move = UP; + } + if (align > score) { + score = align; + move = DIAGONAL; + } else { + csc_score = 0; + } + score_row[j] = score; + csc_row[j] = csc_score; + trace[++pos] = score > 0 ? move : STOP; + } + } + i = m - 1; + j = n - 1; + pos = i * n + j; + backtrack = true; + matches = []; + while (backtrack && i >= 0 && j >= 0) { + switch (trace[pos]) { + case UP: + i--; + pos -= n; + break; + case LEFT: + j--; + pos--; + break; + case DIAGONAL: + matches.push(i + offset); + j--; + i--; + pos -= n + 1; + break; + default: + backtrack = false; + } + } + matches.reverse(); + return matches; + }; + +}).call(this); + +},{"./scorer":6,"path":7}],6:[function(require,module,exports){ +(function() { + var AcronymResult, PathSeparator, Query, basenameScore, coreChars, countDir, doScore, emptyAcronymResult, file_coeff, isMatch, isSeparator, isWordEnd, isWordStart, miss_coeff, opt_char_re, pos_bonus, scoreAcronyms, scoreCharacter, scoreConsecutives, scoreExact, scoreExactMatch, scorePattern, scorePosition, scoreSize, tau_depth, tau_size, truncatedUpperCase, wm; + + PathSeparator = require('path').sep; + + wm = 150; + + pos_bonus = 20; + + tau_depth = 13; + + tau_size = 85; + + file_coeff = 1.2; + + miss_coeff = 0.75; + + opt_char_re = /[ _\-:\/\\]/g; + + exports.coreChars = coreChars = function(query) { + return query.replace(opt_char_re, ''); + }; + + exports.score = function(string, query, prepQuery, allowErrors) { + var score, string_lw; + if (prepQuery == null) { + prepQuery = new Query(query); + } + if (allowErrors == null) { + allowErrors = false; + } + if (!(allowErrors || isMatch(string, prepQuery.core_lw, prepQuery.core_up))) { + return 0; + } + string_lw = string.toLowerCase(); + score = doScore(string, string_lw, prepQuery); + return Math.ceil(basenameScore(string, string_lw, prepQuery, score)); + }; + + Query = (function() { + function Query(query) { + if (!(query != null ? query.length : void 0)) { + return null; + } + this.query = query; + this.query_lw = query.toLowerCase(); + this.core = coreChars(query); + this.core_lw = this.core.toLowerCase(); + this.core_up = truncatedUpperCase(this.core); + this.depth = countDir(query, query.length); + } + + return Query; + + })(); + + exports.prepQuery = function(query) { + return new Query(query); + }; + + exports.isMatch = isMatch = function(subject, query_lw, query_up) { + var i, j, m, n, qj_lw, qj_up, si; + m = subject.length; + n = query_lw.length; + if (!m || n > m) { + return false; + } + i = -1; + j = -1; + while (++j < n) { + qj_lw = query_lw[j]; + qj_up = query_up[j]; + while (++i < m) { + si = subject[i]; + if (si === qj_lw || si === qj_up) { + break; + } + } + if (i === m) { + return false; + } + } + return true; + }; + + doScore = function(subject, subject_lw, prepQuery) { + var acro, acro_score, align, csc_diag, csc_row, csc_score, i, j, m, miss_budget, miss_left, mm, n, pos, query, query_lw, record_miss, score, score_diag, score_row, score_up, si_lw, start, sz; + query = prepQuery.query; + query_lw = prepQuery.query_lw; + m = subject.length; + n = query.length; + acro = scoreAcronyms(subject, subject_lw, query, query_lw); + acro_score = acro.score; + if (acro.count === n) { + return scoreExact(n, m, acro_score, acro.pos); + } + pos = subject_lw.indexOf(query_lw); + if (pos > -1) { + return scoreExactMatch(subject, subject_lw, query, query_lw, pos, n, m); + } + score_row = new Array(n); + csc_row = new Array(n); + sz = scoreSize(n, m); + miss_budget = Math.ceil(miss_coeff * n) + 5; + miss_left = miss_budget; + j = -1; + while (++j < n) { + score_row[j] = 0; + csc_row[j] = 0; + } + i = subject_lw.indexOf(query_lw[0]); + if (i > -1) { + i--; + } + mm = subject_lw.lastIndexOf(query_lw[n - 1], m); + if (mm > i) { + m = mm + 1; + } + while (++i < m) { + score = 0; + score_diag = 0; + csc_diag = 0; + si_lw = subject_lw[i]; + record_miss = true; + j = -1; + while (++j < n) { + score_up = score_row[j]; + if (score_up > score) { + score = score_up; + } + csc_score = 0; + if (query_lw[j] === si_lw) { + start = isWordStart(i, subject, subject_lw); + csc_score = csc_diag > 0 ? csc_diag : scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start); + align = score_diag + scoreCharacter(i, j, start, acro_score, csc_score); + if (align > score) { + score = align; + miss_left = miss_budget; + } else { + if (record_miss && --miss_left <= 0) { + return score_row[n - 1] * sz; + } + record_miss = false; + } + } + score_diag = score_up; + csc_diag = csc_row[j]; + csc_row[j] = csc_score; + score_row[j] = score; + } + } + return score * sz; + }; + + exports.isWordStart = isWordStart = function(pos, subject, subject_lw) { + var curr_s, prev_s; + if (pos === 0) { + return true; + } + curr_s = subject[pos]; + prev_s = subject[pos - 1]; + return isSeparator(curr_s) || isSeparator(prev_s) || (curr_s !== subject_lw[pos] && prev_s === subject_lw[pos - 1]); + }; + + exports.isWordEnd = isWordEnd = function(pos, subject, subject_lw, len) { + var curr_s, next_s; + if (pos === len - 1) { + return true; + } + curr_s = subject[pos]; + next_s = subject[pos + 1]; + return isSeparator(curr_s) || isSeparator(next_s) || (curr_s === subject_lw[pos] && next_s !== subject_lw[pos + 1]); + }; + + isSeparator = function(c) { + return c === ' ' || c === '.' || c === '-' || c === '_' || c === '/' || c === '\\'; + }; + + scorePosition = function(pos) { + var sc; + if (pos < pos_bonus) { + sc = pos_bonus - pos; + return 100 + sc * sc; + } else { + return Math.max(100 + pos_bonus - pos, 0); + } + }; + + scoreSize = function(n, m) { + return tau_size / (tau_size + Math.abs(m - n)); + }; + + scoreExact = function(n, m, quality, pos) { + return 2 * n * (wm * quality + scorePosition(pos)) * scoreSize(n, m); + }; + + exports.scorePattern = scorePattern = function(count, len, sameCase, start, end) { + var bonus, sz; + sz = count; + bonus = 6; + if (sameCase === count) { + bonus += 2; + } + if (start) { + bonus += 3; + } + if (end) { + bonus += 1; + } + if (count === len) { + if (start) { + if (sameCase === len) { + sz += 2; + } else { + sz += 1; + } + } + if (end) { + bonus += 1; + } + } + return sameCase + sz * (sz + bonus); + }; + + exports.scoreCharacter = scoreCharacter = function(i, j, start, acro_score, csc_score) { + var posBonus; + posBonus = scorePosition(i); + if (start) { + return posBonus + wm * ((acro_score > csc_score ? acro_score : csc_score) + 10); + } + return posBonus + wm * csc_score; + }; + + exports.scoreConsecutives = scoreConsecutives = function(subject, subject_lw, query, query_lw, i, j, start) { + var k, m, mi, n, nj, sameCase, startPos, sz; + m = subject.length; + n = query.length; + mi = m - i; + nj = n - j; + k = mi < nj ? mi : nj; + startPos = i; + sameCase = 0; + sz = 0; + if (query[j] === subject[i]) { + sameCase++; + } + while (++sz < k && query_lw[++j] === subject_lw[++i]) { + if (query[j] === subject[i]) { + sameCase++; + } + } + if (sz === 1) { + return 1 + 2 * sameCase; + } + return scorePattern(sz, n, sameCase, start, isWordEnd(i, subject, subject_lw, m)); + }; + + exports.scoreExactMatch = scoreExactMatch = function(subject, subject_lw, query, query_lw, pos, n, m) { + var end, i, pos2, sameCase, start; + start = isWordStart(pos, subject, subject_lw); + if (!start) { + pos2 = subject_lw.indexOf(query_lw, pos + 1); + if (pos2 > -1) { + start = isWordStart(pos2, subject, subject_lw); + if (start) { + pos = pos2; + } + } + } + i = -1; + sameCase = 0; + while (++i < n) { + if (query[pos + i] === subject[i]) { + sameCase++; + } + } + end = isWordEnd(pos + n - 1, subject, subject_lw, m); + return scoreExact(n, m, scorePattern(n, n, sameCase, start, end), pos); + }; + + AcronymResult = (function() { + function AcronymResult(score, pos, count) { + this.score = score; + this.pos = pos; + this.count = count; + } + + return AcronymResult; + + })(); + + emptyAcronymResult = new AcronymResult(0, 0.1, 0); + + exports.scoreAcronyms = scoreAcronyms = function(subject, subject_lw, query, query_lw) { + var count, i, j, m, n, pos, qj_lw, sameCase, score; + m = subject.length; + n = query.length; + if (!(m > 1 && n > 1)) { + return emptyAcronymResult; + } + count = 0; + pos = 0; + sameCase = 0; + i = -1; + j = -1; + while (++j < n) { + qj_lw = query_lw[j]; + while (++i < m) { + if (qj_lw === subject_lw[i] && isWordStart(i, subject, subject_lw)) { + if (query[j] === subject[i]) { + sameCase++; + } + pos += i; + count++; + break; + } + } + if (i === m) { + break; + } + } + if (count < 2) { + return emptyAcronymResult; + } + score = scorePattern(count, n, sameCase, true, false); + return new AcronymResult(score, pos / count, count); + }; + + basenameScore = function(subject, subject_lw, prepQuery, fullPathScore) { + var alpha, basePathScore, basePos, depth, end; + if (fullPathScore === 0) { + return 0; + } + end = subject.length - 1; + while (subject[end] === PathSeparator) { + end--; + } + basePos = subject.lastIndexOf(PathSeparator, end); + if (basePos === -1) { + return fullPathScore; + } + depth = prepQuery.depth; + while (depth-- > 0) { + basePos = subject.lastIndexOf(PathSeparator, basePos - 1); + if (basePos === -1) { + return fullPathScore; + } + } + basePos++; + end++; + basePathScore = doScore(subject.slice(basePos, end), subject_lw.slice(basePos, end), prepQuery); + alpha = 0.5 * tau_depth / (tau_depth + countDir(subject, end + 1)); + return alpha * basePathScore + (1 - alpha) * fullPathScore * scoreSize(0, file_coeff * (end - basePos)); + }; + + exports.countDir = countDir = function(path, end) { + var count, i; + if (end < 1) { + return 0; + } + count = 0; + i = -1; + while (++i < end && path[i] === PathSeparator) { + continue; + } + while (++i < end) { + if (path[i] === PathSeparator) { + count++; + while (++i < end && path[i] === PathSeparator) { + continue; + } + } + } + return count; + }; + + truncatedUpperCase = function(str) { + var char, upper, _i, _len; + upper = ""; + for (_i = 0, _len = str.length; _i < _len; _i++) { + char = str[_i]; + upper += char.toUpperCase()[0]; + } + return upper; + }; + +}).call(this); + +},{"path":7}],7:[function(require,module,exports){ +(function (process){ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +// resolves . and .. elements in a path array with directory names there +// must be no slashes, empty elements, or device names (c:\) in the array +// (so also no leading and trailing slashes - it does not distinguish +// relative and absolute paths) +function normalizeArray(parts, allowAboveRoot) { + // if the path tries to go above the root, `up` ends up > 0 + var up = 0; + for (var i = parts.length - 1; i >= 0; i--) { + var last = parts[i]; + if (last === '.') { + parts.splice(i, 1); + } else if (last === '..') { + parts.splice(i, 1); + up++; + } else if (up) { + parts.splice(i, 1); + up--; + } + } + + // if the path is allowed to go above the root, restore leading ..s + if (allowAboveRoot) { + for (; up--; up) { + parts.unshift('..'); + } + } + + return parts; +} + +// Split a filename into [root, dir, basename, ext], unix version +// 'root' is just a slash, or nothing. +var splitPathRe = + /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/; +var splitPath = function(filename) { + return splitPathRe.exec(filename).slice(1); +}; + +// path.resolve([from ...], to) +// posix version +exports.resolve = function() { + var resolvedPath = '', + resolvedAbsolute = false; + + for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { + var path = (i >= 0) ? arguments[i] : process.cwd(); + + // Skip empty and invalid entries + if (typeof path !== 'string') { + throw new TypeError('Arguments to path.resolve must be strings'); + } else if (!path) { + continue; + } + + resolvedPath = path + '/' + resolvedPath; + resolvedAbsolute = path.charAt(0) === '/'; + } + + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + + // Normalize the path + resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) { + return !!p; + }), !resolvedAbsolute).join('/'); + + return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; +}; + +// path.normalize(path) +// posix version +exports.normalize = function(path) { + var isAbsolute = exports.isAbsolute(path), + trailingSlash = substr(path, -1) === '/'; + + // Normalize the path + path = normalizeArray(filter(path.split('/'), function(p) { + return !!p; + }), !isAbsolute).join('/'); + + if (!path && !isAbsolute) { + path = '.'; + } + if (path && trailingSlash) { + path += '/'; + } + + return (isAbsolute ? '/' : '') + path; +}; + +// posix version +exports.isAbsolute = function(path) { + return path.charAt(0) === '/'; +}; + +// posix version +exports.join = function() { + var paths = Array.prototype.slice.call(arguments, 0); + return exports.normalize(filter(paths, function(p, index) { + if (typeof p !== 'string') { + throw new TypeError('Arguments to path.join must be strings'); + } + return p; + }).join('/')); +}; + + +// path.relative(from, to) +// posix version +exports.relative = function(from, to) { + from = exports.resolve(from).substr(1); + to = exports.resolve(to).substr(1); + + function trim(arr) { + var start = 0; + for (; start < arr.length; start++) { + if (arr[start] !== '') break; + } + + var end = arr.length - 1; + for (; end >= 0; end--) { + if (arr[end] !== '') break; + } + + if (start > end) return []; + return arr.slice(start, end - start + 1); + } + + var fromParts = trim(from.split('/')); + var toParts = trim(to.split('/')); + + var length = Math.min(fromParts.length, toParts.length); + var samePartsLength = length; + for (var i = 0; i < length; i++) { + if (fromParts[i] !== toParts[i]) { + samePartsLength = i; + break; + } + } + + var outputParts = []; + for (var i = samePartsLength; i < fromParts.length; i++) { + outputParts.push('..'); + } + + outputParts = outputParts.concat(toParts.slice(samePartsLength)); + + return outputParts.join('/'); +}; + +exports.sep = '/'; +exports.delimiter = ':'; + +exports.dirname = function(path) { + var result = splitPath(path), + root = result[0], + dir = result[1]; + + if (!root && !dir) { + // No dirname whatsoever + return '.'; + } + + if (dir) { + // It has a dirname, strip trailing slash + dir = dir.substr(0, dir.length - 1); + } + + return root + dir; +}; + + +exports.basename = function(path, ext) { + var f = splitPath(path)[2]; + // TODO: make this comparison case-insensitive on windows? + if (ext && f.substr(-1 * ext.length) === ext) { + f = f.substr(0, f.length - ext.length); + } + return f; +}; + + +exports.extname = function(path) { + return splitPath(path)[3]; +}; + +function filter (xs, f) { + if (xs.filter) return xs.filter(f); + var res = []; + for (var i = 0; i < xs.length; i++) { + if (f(xs[i], i, xs)) res.push(xs[i]); + } + return res; +} + +// String.prototype.substr - negative index don't work in IE8 +var substr = 'ab'.substr(-1) === 'b' + ? function (str, start, len) { return str.substr(start, len) } + : function (str, start, len) { + if (start < 0) start = str.length + start; + return str.substr(start, len); + } +; + +}).call(this,require('_process')) +},{"_process":8}],8:[function(require,module,exports){ +// shim for using process in browser + +var process = module.exports = {}; +var queue = []; +var draining = false; +var currentQueue; +var queueIndex = -1; + +function cleanUpNextTick() { + draining = false; + if (currentQueue.length) { + queue = currentQueue.concat(queue); + } else { + queueIndex = -1; + } + if (queue.length) { + drainQueue(); + } +} + +function drainQueue() { + if (draining) { + return; + } + var timeout = setTimeout(cleanUpNextTick); + draining = true; + + var len = queue.length; + while(len) { + currentQueue = queue; + queue = []; + while (++queueIndex < len) { + if (currentQueue) { + currentQueue[queueIndex].run(); + } + } + queueIndex = -1; + len = queue.length; + } + currentQueue = null; + draining = false; + clearTimeout(timeout); +} + +process.nextTick = function (fun) { + var args = new Array(arguments.length - 1); + if (arguments.length > 1) { + for (var i = 1; i < arguments.length; i++) { + args[i - 1] = arguments[i]; + } + } + queue.push(new Item(fun, args)); + if (queue.length === 1 && !draining) { + setTimeout(drainQueue, 0); + } +}; + +// v8 likes predictible objects +function Item(fun, array) { + this.fun = fun; + this.array = array; +} +Item.prototype.run = function () { + this.fun.apply(null, this.array); +}; +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; +process.version = ''; // empty string to avoid regexp issues +process.versions = {}; + +function noop() {} + +process.on = noop; +process.addListener = noop; +process.once = noop; +process.off = noop; +process.removeListener = noop; +process.removeAllListeners = noop; +process.emit = noop; + +process.binding = function (name) { + throw new Error('process.binding is not supported'); +}; + +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; +process.umask = function() { return 0; }; + +},{}]},{},[1]); diff --git a/vendor/assets/javascripts/fuzzaldrin-plus.min.js b/vendor/assets/javascripts/fuzzaldrin-plus.min.js deleted file mode 100644 index 3f25c2d8373..00000000000 --- a/vendor/assets/javascripts/fuzzaldrin-plus.min.js +++ /dev/null @@ -1 +0,0 @@ -(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0?maxInners:candidates.length;bAllowErrors=!!allowErrors;bKey=key!=null;prepQuery=scorer.prepQuery(query);if(!legacy){for(i=0,len=candidates.length;i0){scoredCandidates.push({candidate:candidate,score:score});if(!--spotLeft){break}}}}else{queryHasSlashes=prepQuery.depth>0;coreQuery=prepQuery.core;for(j=0,len1=candidates.length;j0){scoredCandidates.push({candidate:candidate,score:score})}}}scoredCandidates.sort(sortCandidates);candidates=scoredCandidates.map(pluckCandidates);if(maxResults!=null){candidates=candidates.slice(0,maxResults)}return candidates}}).call(this)},{"./legacy":4,"./scorer":6,"path":7}],2:[function(require,module,exports){(function(){var PathSeparator,filter,legacy_scorer,matcher,prepQueryCache,scorer;scorer=require('./scorer');legacy_scorer=require('./legacy');filter=require('./filter');matcher=require('./matcher');PathSeparator=require('path').sep;prepQueryCache=null;module.exports={filter:function(candidates,query,options){if(!((query!=null?query.length:void 0)&&(candidates!=null?candidates.length:void 0))){return[]}return filter(candidates,query,options)},prepQuery:function(query){return scorer.prepQuery(query)},score:function(string,query,prepQuery,arg){var allowErrors,coreQuery,legacy,queryHasSlashes,ref,score;ref=arg!=null?arg:{},allowErrors=ref.allowErrors,legacy=ref.legacy;if(!((string!=null?string.length:void 0)&&(query!=null?query.length:void 0))){return 0}if(prepQuery==null){prepQuery=prepQueryCache&&prepQueryCache.query===query?prepQueryCache:(prepQueryCache=scorer.prepQuery(query))}if(!legacy){score=scorer.score(string,query,prepQuery,!!allowErrors)}else{queryHasSlashes=prepQuery.depth>0;coreQuery=prepQuery.core;score=legacy_scorer.score(string,coreQuery,queryHasSlashes);if(!queryHasSlashes){score=legacy_scorer.basenameScore(string,coreQuery,score)}}return score},match:function(string,query,prepQuery,arg){var allowErrors,baseMatches,i,matches,query_lw,ref,results,string_lw;allowErrors=(arg!=null?arg:{}).allowErrors;if(!string){return[]}if(!query){return[]}if(string===query){return(function(){results=[];for(var i=0,ref=string.length;0<=ref?iref;0<=ref?i++:i--){results.push(i)}return results}).apply(this)}if(prepQuery==null){prepQuery=prepQueryCache&&prepQueryCache.query===query?prepQueryCache:(prepQueryCache=scorer.prepQuery(query))}if(!(allowErrors||scorer.isMatch(string,prepQuery.core_lw,prepQuery.core_up))){return[]}string_lw=string.toLowerCase();query_lw=prepQuery.query_lw;matches=matcher.match(string,string_lw,prepQuery);if(matches.length===0){return matches}if(string.indexOf(PathSeparator)>-1){baseMatches=matcher.basenameMatch(string,string_lw,prepQuery);matches=matcher.mergeMatches(matches,baseMatches)}return matches}}}).call(this)},{"./filter":1,"./legacy":4,"./matcher":5,"./scorer":6,"path":7}],3:[function(require,module,exports){fuzzaldrinPlus=require('./fuzzaldrin')},{"./fuzzaldrin":2}],4:[function(require,module,exports){(function(){var PathSeparator,queryIsLastPathSegment;PathSeparator=require('path').sep;exports.basenameScore=function(string,query,score){var base,depth,index,lastCharacter,segmentCount,slashCount;index=string.length-1;while(string[index]===PathSeparator){index--}slashCount=0;lastCharacter=index;base=null;while(index>=0){if(string[index]===PathSeparator){slashCount++;if(base==null){base=string.substring(index+1,lastCharacter+1)}}else if(index===0){if(lastCharacterref;stringOffset<=ref?i++:i--){results.push(i)}return results}).apply(this)}queryLength=query.length;stringLength=string.length;indexInQuery=0;indexInString=0;matches=[];while(indexInQuery0){basePos=subject.lastIndexOf(PathSeparator,basePos-1);if(basePos===-1){return[]}}basePos++;end++;return exports.match(subject.slice(basePos,end),subject_lw.slice(basePos,end),prepQuery,basePos)};exports.mergeMatches=function(a,b){var ai,bj,i,j,m,n,out;m=a.length;n=b.length;if(n===0){return a.slice()}if(m===0){return b.slice()}i=-1;j=0;bj=b[j];out=[];while(++i0?csc_diag:scorer.scoreConsecutives(subject,subject_lw,query,query_lw,i,j,start);align=score_diag+scorer.scoreCharacter(i,j,start,acro_score,csc_score)}score_up=score_row[j];csc_diag=csc_row[j];if(score>score_up){move=LEFT}else{score=score_up;move=UP}if(align>score){score=align;move=DIAGONAL}else{csc_score=0}score_row[j]=score;csc_row[j]=csc_score;trace[++pos]=score>0?move:STOP}}i=m-1;j=n-1;pos=i*n+j;backtrack=true;matches=[];while(backtrack&&i>=0&&j>=0){switch(trace[pos]){case UP:i--;pos-=n;break;case LEFT:j--;pos--;break;case DIAGONAL:matches.push(i+offset);j--;i--;pos-=n+1;break;default:backtrack=false}}matches.reverse();return matches}}).call(this)},{"./scorer":6,"path":7}],6:[function(require,module,exports){(function(){var AcronymResult,PathSeparator,Query,basenameScore,coreChars,countDir,doScore,emptyAcronymResult,file_coeff,isMatch,isSeparator,isWordEnd,isWordStart,miss_coeff,opt_char_re,pos_bonus,scoreAcronyms,scoreCharacter,scoreConsecutives,scoreExact,scoreExactMatch,scorePattern,scorePosition,scoreSize,tau_depth,tau_size,truncatedUpperCase,wm;PathSeparator=require('path').sep;wm=150;pos_bonus=20;tau_depth=13;tau_size=85;file_coeff=1.2;miss_coeff=0.75;opt_char_re=/[ _\-:\/\\]/g;exports.coreChars=coreChars=function(query){return query.replace(opt_char_re,'')};exports.score=function(string,query,prepQuery,allowErrors){var score,string_lw;if(prepQuery==null){prepQuery=new Query(query)}if(allowErrors==null){allowErrors=false}if(!(allowErrors||isMatch(string,prepQuery.core_lw,prepQuery.core_up))){return 0}string_lw=string.toLowerCase();score=doScore(string,string_lw,prepQuery);return Math.ceil(basenameScore(string,string_lw,prepQuery,score))};Query=(function(){function Query(query){if(!(query!=null?query.length:void 0)){return null}this.query=query;this.query_lw=query.toLowerCase();this.core=coreChars(query);this.core_lw=this.core.toLowerCase();this.core_up=truncatedUpperCase(this.core);this.depth=countDir(query,query.length)}return Query})();exports.prepQuery=function(query){return new Query(query)};exports.isMatch=isMatch=function(subject,query_lw,query_up){var i,j,m,n,qj_lw,qj_up,si;m=subject.length;n=query_lw.length;if(!m||!n||n>m){return false}i=-1;j=-1;while(++j-1){return scoreExactMatch(subject,subject_lw,query,query_lw,pos,n,m)}score_row=new Array(n);csc_row=new Array(n);sz=scoreSize(n,m);miss_budget=Math.ceil(miss_coeff*n)+5;miss_left=miss_budget;j=-1;while(++j-1){i--}mm=subject_lw.lastIndexOf(query_lw[n-1],m);if(mm>i){m=mm+1}while(++iscore){score=score_up}csc_score=0;if(query_lw[j]===si_lw){start=isWordStart(i,subject,subject_lw);csc_score=csc_diag>0?csc_diag:scoreConsecutives(subject,subject_lw,query,query_lw,i,j,start);align=score_diag+scoreCharacter(i,j,start,acro_score,csc_score);if(align>score){score=align;miss_left=miss_budget}else{if(record_miss&&--miss_left<=0){return score_row[n-1]*sz}record_miss=false}}score_diag=score_up;csc_diag=csc_row[j];csc_row[j]=csc_score;score_row[j]=score}}return score*sz};exports.isWordStart=isWordStart=function(pos,subject,subject_lw){var curr_s,prev_s;if(pos===0){return true}curr_s=subject[pos];prev_s=subject[pos-1];return isSeparator(curr_s)||isSeparator(prev_s)||(curr_s!==subject_lw[pos]&&prev_s===subject_lw[pos-1])};exports.isWordEnd=isWordEnd=function(pos,subject,subject_lw,len){var curr_s,next_s;if(pos===len-1){return true}curr_s=subject[pos];next_s=subject[pos+1];return isSeparator(curr_s)||isSeparator(next_s)||(curr_s===subject_lw[pos]&&next_s!==subject_lw[pos+1])};isSeparator=function(c){return c===' '||c==='.'||c==='-'||c==='_'||c==='/'||c==='\\'};scorePosition=function(pos){var sc;if(poscsc_score?acro_score:csc_score)+10)}return posBonus+wm*csc_score};exports.scoreConsecutives=scoreConsecutives=function(subject,subject_lw,query,query_lw,i,j,start){var k,m,mi,n,nj,sameCase,startPos,sz;m=subject.length;n=query.length;mi=m-i;nj=n-j;k=mi-1){start=isWordStart(pos2,subject,subject_lw);if(start){pos=pos2}}}i=-1;sameCase=0;while(++i1&&n>1)){return emptyAcronymResult}count=0;pos=0;sameCase=0;i=-1;j=-1;while(++j0){basePos=subject.lastIndexOf(PathSeparator,basePos-1);if(basePos===-1){return fullPathScore}}basePos++;end++;basePathScore=doScore(subject.slice(basePos,end),subject_lw.slice(basePos,end),prepQuery);alpha=0.5*tau_depth/(tau_depth+countDir(subject,end+1));return alpha*basePathScore+(1-alpha)*fullPathScore*scoreSize(0,file_coeff*(end-basePos))};exports.countDir=countDir=function(path,end){var count,i;if(end<1){return 0}count=0;i=-1;while(++i=0;i--){var last=parts[i];if(last==='.'){parts.splice(i,1)}else if(last==='..'){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up--;up){parts.unshift('..')}}return parts}var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;var splitPath=function(filename){return splitPathRe.exec(filename).slice(1)};exports.resolve=function(){var resolvedPath='',resolvedAbsolute=false;for(var i=arguments.length-1;i>=-1&&!resolvedAbsolute;i--){var path=(i>=0)?arguments[i]:process.cwd();if(typeof path!=='string'){throw new TypeError('Arguments to path.resolve must be strings');}else if(!path){continue}resolvedPath=path+'/'+resolvedPath;resolvedAbsolute=path.charAt(0)==='/'}resolvedPath=normalizeArray(filter(resolvedPath.split('/'),function(p){return!!p}),!resolvedAbsolute).join('/');return((resolvedAbsolute?'/':'')+resolvedPath)||'.'};exports.normalize=function(path){var isAbsolute=exports.isAbsolute(path),trailingSlash=substr(path,-1)==='/';path=normalizeArray(filter(path.split('/'),function(p){return!!p}),!isAbsolute).join('/');if(!path&&!isAbsolute){path='.'}if(path&&trailingSlash){path+='/'}return(isAbsolute?'/':'')+path};exports.isAbsolute=function(path){return path.charAt(0)==='/'};exports.join=function(){var paths=Array.prototype.slice.call(arguments,0);return exports.normalize(filter(paths,function(p,index){if(typeof p!=='string'){throw new TypeError('Arguments to path.join must be strings');}return p}).join('/'))};exports.relative=function(from,to){from=exports.resolve(from).substr(1);to=exports.resolve(to).substr(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=='')break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split('/'));var toParts=trim(to.split('/'));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i1){for(var i=1;i Date: Mon, 11 Jan 2016 14:44:31 -0500 Subject: Setup new fork link in sidebar and routes. #2406 --- app/controllers/projects/forks_controller.rb | 3 +++ app/helpers/blob_helper.rb | 4 ++-- app/helpers/projects_helper.rb | 2 +- app/views/layouts/nav/_project.html.haml | 7 +++++++ app/views/projects/buttons/_dropdown.html.haml | 2 +- app/views/projects/buttons/_fork.html.haml | 2 +- app/views/projects/forks/new.html.haml | 2 +- app/views/projects/tree/_tree_header.html.haml | 6 +++--- config/routes.rb | 2 +- 9 files changed, 20 insertions(+), 10 deletions(-) diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb index 750181f0c19..92c91fc539a 100644 --- a/app/controllers/projects/forks_controller.rb +++ b/app/controllers/projects/forks_controller.rb @@ -3,6 +3,9 @@ class Projects::ForksController < Projects::ApplicationController before_action :require_non_empty_project before_action :authorize_download_code! + def index + end + def new @namespaces = current_user.manageable_namespaces @namespaces.delete(@project.namespace) diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index d31d4cde08f..9fa10d12dfa 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -47,7 +47,7 @@ module BlobHelper notice: edit_in_new_fork_notice, notice_now: edit_in_new_fork_notice_now } - fork_path = namespace_project_fork_path(project.namespace, project, namespace_key: current_user.namespace.id, + fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) link_to "Edit", fork_path, class: 'btn btn-small', method: :post @@ -73,7 +73,7 @@ module BlobHelper notice: edit_in_new_fork_notice + " Try to #{action} this file again.", notice_now: edit_in_new_fork_notice_now } - fork_path = namespace_project_fork_path(project.namespace, project, namespace_key: current_user.namespace.id, + fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) link_to label, fork_path, class: "btn btn-#{btn_class}", method: :post diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 77ba612548a..48598683e6d 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -116,7 +116,7 @@ module ProjectsHelper private def get_project_nav_tabs(project, current_user) - nav_tabs = [:home] + nav_tabs = [:home, :forks] if !project.empty_repo? && can?(current_user, :download_code, project) nav_tabs << [:files, :commits, :network, :graphs] diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 270ccfd387f..7fc87caf40d 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -98,6 +98,13 @@ %span Wiki + - if project_nav_tab? :forks + = nav_link(controller: :forks, action: :index) do + = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'shortcuts-wiki' do + = icon('book fw') + %span + Forks + - if project_nav_tab? :snippets = nav_link(controller: :snippets) do = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 511863d774e..e7c85edff96 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -46,7 +46,7 @@ - continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master'), notice: edit_in_new_fork_notice, notice_now: edit_in_new_fork_notice_now } - - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id, + - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id, continue: continue_params) = link_to fork_path, method: :post do = icon('file fw') diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 133531887a2..f65c612bc66 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -9,7 +9,7 @@ %span.count = @project.forks_count - else - = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has_tooltip' do + = link_to new_namespace_project_forks_path(@project.namespace, @project), title: "Fork project", class: 'btn has_tooltip' do = icon('code-fork fw') Fork %div.count-with-arrow diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml index 8a2c027a455..edabc2d3b44 100644 --- a/app/views/projects/forks/new.html.haml +++ b/app/views/projects/forks/new.html.haml @@ -22,7 +22,7 @@ - else .fork-thumbnail - = link_to namespace_project_fork_path(@project.namespace, @project, namespace_key: namespace.id), title: "Fork here", method: "POST", class: 'has_tooltip' do + = link_to namespace_project_forks_path(@project.namespace, @project, namespace_key: namespace.id), title: "Fork here", method: "POST", class: 'has_tooltip' do = image_tag namespace_icon(namespace, 100) .caption %strong diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 3343288ad2b..3eb626e6dca 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -40,7 +40,7 @@ - continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @id), notice: edit_in_new_fork_notice, notice_now: edit_in_new_fork_notice_now } - - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id, + - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id, continue: continue_params) = link_to fork_path, method: :post do = icon('pencil fw') @@ -49,7 +49,7 @@ - continue_params = { to: request.fullpath, notice: edit_in_new_fork_notice + " Try to upload a file again.", notice_now: edit_in_new_fork_notice_now } - - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id, + - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id, continue: continue_params) = link_to fork_path, method: :post do = icon('file fw') @@ -58,7 +58,7 @@ - continue_params = { to: request.fullpath, notice: edit_in_new_fork_notice + " Try to create a new directory again.", notice_now: edit_in_new_fork_notice_now } - - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id, + - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id, continue: continue_params) = link_to fork_path, method: :post do = icon('folder fw') diff --git a/config/routes.rb b/config/routes.rb index 75418db8d25..fdfdb449085 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -554,7 +554,7 @@ Rails.application.routes.draw do end end - resource :fork, only: [:new, :create] + resources :forks, only: [:index, :new, :create] resource :import, only: [:new, :create, :show] resources :refs, only: [] do -- cgit v1.2.1 From 2b05bf4be5c01c2b6b09a8c0792d1c9dd8afe6e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Mon, 11 Jan 2016 15:36:42 -0500 Subject: Use Project creator's avatar in fork listing. #2406 --- app/controllers/projects/forks_controller.rb | 3 ++- app/views/projects/buttons/_fork.html.haml | 2 +- app/views/projects/forks/index.html.haml | 3 +++ app/views/shared/projects/_list.html.haml | 3 ++- app/views/shared/projects/_project.html.haml | 5 ++++- 5 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 app/views/projects/forks/index.html.haml diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb index 92c91fc539a..2e01352169d 100644 --- a/app/controllers/projects/forks_controller.rb +++ b/app/controllers/projects/forks_controller.rb @@ -4,6 +4,7 @@ class Projects::ForksController < Projects::ApplicationController before_action :authorize_download_code! def index + @forked_projects = project.forks.includes(:creator) end def new @@ -13,7 +14,7 @@ class Projects::ForksController < Projects::ApplicationController def create namespace = Namespace.find(params[:namespace_key]) - + @forked_project = namespace.projects.find_by(path: project.path) @forked_project = nil unless @forked_project && @forked_project.forked_from_project == project diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index f65c612bc66..133531887a2 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -9,7 +9,7 @@ %span.count = @project.forks_count - else - = link_to new_namespace_project_forks_path(@project.namespace, @project), title: "Fork project", class: 'btn has_tooltip' do + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has_tooltip' do = icon('code-fork fw') Fork %div.count-with-arrow diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml new file mode 100644 index 00000000000..8ae7fe22e1e --- /dev/null +++ b/app/views/projects/forks/index.html.haml @@ -0,0 +1,3 @@ +.projects-list-holder + + = render 'shared/projects/list', projects: @forked_projects, use_creator_avatar: true diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index e5ffe1e29ae..c0a7b9f08fa 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -1,5 +1,6 @@ - projects_limit = 20 unless local_assigns[:projects_limit] - avatar = true unless local_assigns[:avatar] == false +- use_creator_avatar = false unless local_assigns[:use_creator_avatar] == true - stars = true unless local_assigns[:stars] == false - ci = false unless local_assigns[:ci] == true - skip_namespace = false unless local_assigns[:skip_namespace] == true @@ -8,7 +9,7 @@ - projects.each_with_index do |project, i| - css_class = (i >= projects_limit) ? 'hide' : nil = render "shared/projects/project", project: project, skip_namespace: skip_namespace, - avatar: avatar, stars: stars, css_class: css_class, ci: ci + avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar - if projects.size > projects_limit %li.bottom.center diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 5db8056b77c..9bf45d3ce33 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -13,7 +13,10 @@ = link_to project_path(project), class: dom_class(project) do - if avatar .dash-project-avatar - = project_icon(project, alt: '', class: 'avatar project-avatar s46') + - if use_creator_avatar + = image_tag avatar_icon(project.creator.email, 46), class: "avatar s46", alt:'' + - else + = project_icon(project, alt: '', class: 'avatar project-avatar s46') %span.project-full-name %span.namespace-name - if project.namespace && !skip_namespace -- cgit v1.2.1 From b2e5b57fab4d73a5ad043d82457268fdb3864a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Mon, 11 Jan 2016 19:16:29 -0500 Subject: Add fork button and implement ability to distinguish between public and protected forks. #2406 --- app/controllers/projects/forks_controller.rb | 5 ++++- app/views/projects/forks/index.html.haml | 23 ++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb index 2e01352169d..bcb64479cbf 100644 --- a/app/controllers/projects/forks_controller.rb +++ b/app/controllers/projects/forks_controller.rb @@ -4,7 +4,10 @@ class Projects::ForksController < Projects::ApplicationController before_action :authorize_download_code! def index - @forked_projects = project.forks.includes(:creator) + @all_forks = project.forks.includes(:creator) + @public_forks, @protected_forks = @all_forks.partition do |project| + can?(current_user, :read_project, project) + end end def new diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index 8ae7fe22e1e..d050b2f735a 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -1,3 +1,24 @@ +.gray-content-block.top-block.white + - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'pull-right btn btn-new' do + = icon('code-fork fw') + Fork + - else + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'pull-right btn btn-new' do + = icon('code-fork fw') + Fork + + .oneline + - public_count = @public_forks.size + - protected_count = @protected_forks.size + - full_count_title = ["#{public_count} public", "#{protected_count} protected"].join(' and ') + == #{pluralize(@all_forks.size, 'fork')}: #{full_count_title} + .projects-list-holder + = render 'shared/projects/list', projects: @public_forks, use_creator_avatar: true - = render 'shared/projects/list', projects: @forked_projects, use_creator_avatar: true + - if protected_count > 0 + %ul.projects-list + %li.project-row + %strong= pluralize(protected_count, 'protected fork') + %span you have no access to. -- cgit v1.2.1 From 3b75d65bd3e1bb53b0ce9755323b30221d6a4649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Mon, 11 Jan 2016 19:44:05 -0500 Subject: Show forks counter on listing. #2406 --- app/views/projects/forks/index.html.haml | 3 ++- app/views/shared/projects/_list.html.haml | 4 +++- app/views/shared/projects/_project.html.haml | 5 +++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index d050b2f735a..46ec7a5d22a 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -15,7 +15,8 @@ == #{pluralize(@all_forks.size, 'fork')}: #{full_count_title} .projects-list-holder - = render 'shared/projects/list', projects: @public_forks, use_creator_avatar: true + = render 'shared/projects/list', projects: @public_forks, use_creator_avatar: true, + forks: true - if protected_count > 0 %ul.projects-list diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index c0a7b9f08fa..0c774d217c3 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -2,6 +2,7 @@ - avatar = true unless local_assigns[:avatar] == false - use_creator_avatar = false unless local_assigns[:use_creator_avatar] == true - stars = true unless local_assigns[:stars] == false +- forks = false unless local_assigns[:forks] == true - ci = false unless local_assigns[:ci] == true - skip_namespace = false unless local_assigns[:skip_namespace] == true @@ -9,7 +10,8 @@ - projects.each_with_index do |project, i| - css_class = (i >= projects_limit) ? 'hide' : nil = render "shared/projects/project", project: project, skip_namespace: skip_namespace, - avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar + avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar, + forks: forks - if projects.size > projects_limit %li.bottom.center diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 9bf45d3ce33..96ecb9dadb6 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -1,5 +1,6 @@ - avatar = true unless local_assigns[:avatar] == false - stars = true unless local_assigns[:stars] == false +- forks = false unless local_assigns[:forks] == true - ci = false unless local_assigns[:ci] == true - skip_namespace = false unless local_assigns[:skip_namespace] == true - css_class = '' unless local_assigns[:css_class] @@ -29,6 +30,10 @@ - if ci_commit = render_ci_status(ci_commit)   + - if forks + %span + %i.fa.fa-code-fork + = project.forks_count - if stars %span %i.fa.fa-star -- cgit v1.2.1 From 55707f95896050da638db3c59047e6896b0c0968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Mon, 11 Jan 2016 20:45:11 -0500 Subject: Use adequate icon for Forks link on sidebar. #2406 --- app/views/layouts/nav/_project.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 7fc87caf40d..292faf0e7eb 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -101,7 +101,7 @@ - if project_nav_tab? :forks = nav_link(controller: :forks, action: :index) do = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'shortcuts-wiki' do - = icon('book fw') + = icon('code-fork fw') %span Forks -- cgit v1.2.1 From f0b7dcba30c9041c5d004ee8fa4bbd7652125ed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Mon, 11 Jan 2016 21:38:31 -0500 Subject: Add ability to filter by namespace. #2406 --- app/assets/javascripts/projects_list.js.coffee | 4 +++- app/views/projects/forks/index.html.haml | 28 ++++++++++++++++---------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee index f2887af190b..b71509dbc5a 100644 --- a/app/assets/javascripts/projects_list.js.coffee +++ b/app/assets/javascripts/projects_list.js.coffee @@ -9,11 +9,13 @@ class @ProjectsList $(".projects-list-filter").keyup -> terms = $(this).val() uiBox = $('div.projects-list-holder') + filterSelector = $(this).data('filter-selector') || 'span.filter-title' + if terms == "" || terms == undefined uiBox.find("ul.projects-list li").show() else uiBox.find("ul.projects-list li").each (index) -> - name = $(this).find("span.filter-title").text() + name = $(this).find(filterSelector).text() if name.toLowerCase().search(terms.toLowerCase()) == -1 $(this).hide() diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index 46ec7a5d22a..2150e2e42fb 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -1,19 +1,25 @@ -.gray-content-block.top-block.white - - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'pull-right btn btn-new' do - = icon('code-fork fw') - Fork - - else - = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'pull-right btn btn-new' do - = icon('code-fork fw') - Fork - - .oneline +.gray-content-block.top-block.clearfix.white + .pull-left - public_count = @public_forks.size - protected_count = @protected_forks.size - full_count_title = ["#{public_count} public", "#{protected_count} protected"].join(' and ') == #{pluralize(@all_forks.size, 'fork')}: #{full_count_title} + .pull-right + .projects-search-form + = search_field_tag :filter_projects, nil, placeholder: 'Search forks', class: 'projects-list-filter form-control', + spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' } + + - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'pull-right btn btn-new' do + = icon('code-fork fw') + Fork + - else + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'pull-right btn btn-new' do + = icon('code-fork fw') + Fork + + .projects-list-holder = render 'shared/projects/list', projects: @public_forks, use_creator_avatar: true, forks: true -- cgit v1.2.1 From e4d26966912265b356bca8c9a987956200c24605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Mon, 11 Jan 2016 21:50:02 -0500 Subject: Fix broken specs. #2406 --- app/helpers/blob_helper.rb | 6 ++---- spec/routing/project_routing_spec.rb | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 9fa10d12dfa..32bfee41d13 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -47,8 +47,7 @@ module BlobHelper notice: edit_in_new_fork_notice, notice_now: edit_in_new_fork_notice_now } - fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, - continue: continue_params) + fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) link_to "Edit", fork_path, class: 'btn btn-small', method: :post end @@ -73,8 +72,7 @@ module BlobHelper notice: edit_in_new_fork_notice + " Try to #{action} this file again.", notice_now: edit_in_new_fork_notice_now } - fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, - continue: continue_params) + fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) link_to label, fork_path, class: "btn btn-#{btn_class}", method: :post end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 22ba25217f0..22937226fce 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -496,11 +496,11 @@ end describe Projects::ForksController, 'routing' do it 'to #new' do - expect(get('/gitlab/gitlabhq/fork/new')).to route_to('projects/forks#new', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(get('/gitlab/gitlabhq/forks/new')).to route_to('projects/forks#new', namespace_id: 'gitlab', project_id: 'gitlabhq') end it 'to #create' do - expect(post('/gitlab/gitlabhq/fork')).to route_to('projects/forks#create', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(post('/gitlab/gitlabhq/forks')).to route_to('projects/forks#create', namespace_id: 'gitlab', project_id: 'gitlabhq') end end -- cgit v1.2.1 From b6170d2d8b1017ce135c17916a03ebcc2d3bcf67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 12 Jan 2016 20:07:59 -0500 Subject: Show last commit as description. #2406 --- app/views/projects/forks/index.html.haml | 2 +- app/views/shared/projects/_list.html.haml | 3 ++- app/views/shared/projects/_project.html.haml | 9 +++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index 2150e2e42fb..fabd6b573a2 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -22,7 +22,7 @@ .projects-list-holder = render 'shared/projects/list', projects: @public_forks, use_creator_avatar: true, - forks: true + forks: true, show_last_commit_as_description: true - if protected_count > 0 %ul.projects-list diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index 0c774d217c3..b3f45373f6b 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -5,13 +5,14 @@ - forks = false unless local_assigns[:forks] == true - ci = false unless local_assigns[:ci] == true - skip_namespace = false unless local_assigns[:skip_namespace] == true +- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true %ul.projects-list - projects.each_with_index do |project, i| - css_class = (i >= projects_limit) ? 'hide' : nil = render "shared/projects/project", project: project, skip_namespace: skip_namespace, avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar, - forks: forks + forks: forks, show_last_commit_as_description: show_last_commit_as_description - if projects.size > projects_limit %li.bottom.center diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 96ecb9dadb6..c1dcdb9f0b2 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -4,7 +4,8 @@ - ci = false unless local_assigns[:ci] == true - skip_namespace = false unless local_assigns[:skip_namespace] == true - css_class = '' unless local_assigns[:css_class] -- css_class += " no-description" unless project.description.present? +- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true +- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description - ci_commit = project.ci_commit(project.commit.sha) if ci && !project.empty_repo? && project.commit - cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.2'] - cache_key.push(ci_commit.status) if ci_commit @@ -38,6 +39,10 @@ %span %i.fa.fa-star = project.star_count - - if project.description.present? + - if show_last_commit_as_description + .project-description + = link_to_gfm project.commit.title, namespace_project_commit_path(project.namespace, project, project.commit), + class: "commit-row-message" + - elsif project.description.present? .project-description = markdown(project.description, pipeline: :description) -- cgit v1.2.1 From b0a5a99e9652d6ad9898cd4bd08363516fb15055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 12 Jan 2016 22:14:04 -0500 Subject: Little update to copy and custom content for empty results. #2406 --- app/views/projects/forks/index.html.haml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index fabd6b573a2..985924ad052 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -2,7 +2,7 @@ .pull-left - public_count = @public_forks.size - protected_count = @protected_forks.size - - full_count_title = ["#{public_count} public", "#{protected_count} protected"].join(' and ') + - full_count_title = "#{public_count} public and #{protected_count} private" == #{pluralize(@all_forks.size, 'fork')}: #{full_count_title} .pull-right @@ -21,11 +21,16 @@ .projects-list-holder - = render 'shared/projects/list', projects: @public_forks, use_creator_avatar: true, - forks: true, show_last_commit_as_description: true + - if @public_forks.blank? + %ul.content-list + %li + .nothing-here-block No forks to show + - else + = render 'shared/projects/list', projects: @public_forks, use_creator_avatar: true, + forks: true, show_last_commit_as_description: true - - if protected_count > 0 - %ul.projects-list - %li.project-row - %strong= pluralize(protected_count, 'protected fork') - %span you have no access to. + - if protected_count > 0 + %ul.projects-list + %li.project-row + %strong= pluralize(protected_count, 'private fork') + %span you have no access to. -- cgit v1.2.1 From 5cba592469dcec8c5df2248ca3a1deec83bd3913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 13 Jan 2016 12:10:30 -0500 Subject: WIP: Add sort dropdown. #2406 --- app/views/projects/forks/index.html.haml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index 985924ad052..09406ce9e4b 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -10,6 +10,26 @@ = search_field_tag :filter_projects, nil, placeholder: 'Search forks', class: 'projects-list-filter form-control', spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' } + .dropdown.inline.prepend-left-10 + %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %span.light sort: + - if @sort.present? + = sort_options_hash[@sort] + - else + = sort_title_recently_created + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right + %li + = link_to page_filter_path(sort: sort_value_recently_created) do + = sort_title_recently_created + = link_to page_filter_path(sort: sort_value_oldest_created) do + = sort_title_oldest_created + = link_to page_filter_path(sort: sort_value_recently_updated) do + = sort_title_recently_updated + = link_to page_filter_path(sort: sort_value_oldest_updated) do + = sort_title_oldest_updated + + .fork_link.inline - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'pull-right btn btn-new' do = icon('code-fork fw') -- cgit v1.2.1 From 8f0eee06386c052a9843b9d8b1e702277ccd76ce Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Wed, 13 Jan 2016 12:28:30 -0500 Subject: changing projects css --- app/assets/stylesheets/pages/projects.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 003a4c22f20..8c685c08eaf 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -398,7 +398,6 @@ pre.light-well { .btn { display: inline-block; - width: 135px; } } -- cgit v1.2.1 From af501845dd719c2478a3c5f1bb8ead1f4b6c90b4 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Wed, 13 Jan 2016 13:15:53 -0500 Subject: aligns menu at top on one row --- app/assets/stylesheets/pages/projects.scss | 16 ++++++++++++++++ app/views/projects/forks/index.html.haml | 20 ++++++++++---------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 8c685c08eaf..7c3587622c4 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -391,6 +391,22 @@ pre.light-well { padding-bottom: 0; margin-bottom: 0px; + &.fork-search-form { + margin: 0; + margin-top: -$gl-padding; + padding-bottom: 0; + width: 540px; + + input { + width: calc(100% - 277px); + } + + .fork_link { + float: right; + margin-left: $gl-padding; + } + } + input { display: inline-block; width: calc(100% - 151px); diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index 09406ce9e4b..24efc7b3097 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -6,7 +6,7 @@ == #{pluralize(@all_forks.size, 'fork')}: #{full_count_title} .pull-right - .projects-search-form + .projects-search-form.fork-search-form = search_field_tag :filter_projects, nil, placeholder: 'Search forks', class: 'projects-list-filter form-control', spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' } @@ -29,15 +29,15 @@ = link_to page_filter_path(sort: sort_value_oldest_updated) do = sort_title_oldest_updated - .fork_link.inline - - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'pull-right btn btn-new' do - = icon('code-fork fw') - Fork - - else - = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'pull-right btn btn-new' do - = icon('code-fork fw') - Fork + .fork_link.inline + - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'pull-right btn btn-new' do + = icon('code-fork fw') + Fork + - else + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'pull-right btn btn-new' do + = icon('code-fork fw') + Fork .projects-list-holder -- cgit v1.2.1 From 40e1ad90526ad35e1d5eba74c0b1d99b6a8ae89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Thu, 14 Jan 2016 19:58:57 -0500 Subject: Add ability to sort forks. #2406 --- app/controllers/projects/forks_controller.rb | 2 ++ app/views/projects/forks/index.html.haml | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb index bcb64479cbf..f6fabba26f9 100644 --- a/app/controllers/projects/forks_controller.rb +++ b/app/controllers/projects/forks_controller.rb @@ -5,6 +5,8 @@ class Projects::ForksController < Projects::ApplicationController def index @all_forks = project.forks.includes(:creator) + @all_forks = @all_forks.sort(params[:sort]) if params[:sort] + @public_forks, @protected_forks = @all_forks.partition do |project| can?(current_user, :read_project, project) end diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index 24efc7b3097..10ab5c44af1 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -20,13 +20,14 @@ %b.caret %ul.dropdown-menu.dropdown-menu-align-right %li - = link_to page_filter_path(sort: sort_value_recently_created) do + - excluded_filters = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id] + = link_to page_filter_path(sort: sort_value_recently_created, without: excluded_filters) do = sort_title_recently_created - = link_to page_filter_path(sort: sort_value_oldest_created) do + = link_to page_filter_path(sort: sort_value_oldest_created, without: excluded_filters) do = sort_title_oldest_created - = link_to page_filter_path(sort: sort_value_recently_updated) do + = link_to page_filter_path(sort: sort_value_recently_updated, without: excluded_filters) do = sort_title_recently_updated - = link_to page_filter_path(sort: sort_value_oldest_updated) do + = link_to page_filter_path(sort: sort_value_oldest_updated, without: excluded_filters) do = sort_title_oldest_updated .fork_link.inline -- cgit v1.2.1 From 3278c5b073a1f3e6d48f246525178863959203af Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Wed, 20 Jan 2016 00:15:55 -0500 Subject: Adds disabled comment field and disabled avatar. --- app/assets/stylesheets/pages/merge_requests.scss | 36 ++++++++++++++++++++++ .../projects/notes/_notes_with_form.html.haml | 10 ++++++ 2 files changed, 46 insertions(+) diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 75f2ae80a92..6c1272d5d8c 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -201,3 +201,39 @@ .mr-source-target { line-height: 31px; } + +.disabled-comment-area { + padding: 16px 0; + + .disabled-profile { + width: 40px; + height: 40px; + background: #CCC; + border-radius: 20px; + display: inline-block; + margin-right: 10px; + } + + .disabled-comment { + background: #F3F3F3; + display: inline-block; + vertical-align: top; + height: 200px; + border-radius: 4px; + border: 1px solid #CCC; + padding-top: 90px; + text-align: center; + right: 20px; + position: absolute; + left: 70px; + margin-bottom: 20px; + + span { + color: #B2B2B2; + + a { + color: $md-link-color; + } + } + } +} \ No newline at end of file diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index eb378b42603..910eb6cf66e 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -5,6 +5,16 @@ .js-main-target-form - if can? current_user, :create_note, @project = render "projects/notes/form", view: diff_view +- else + .disabled-comment-area + .disabled-profile + .disabled-comment + %span + Please + = link_to "register",new_user_session_path + or + = link_to "login",new_user_session_path + to post a comment :javascript var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") -- cgit v1.2.1 From 833a0f4d9fd4ec8a264812cc42ca52d30d325e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Thu, 14 Jan 2016 21:05:33 -0500 Subject: Add icon for private forks notice. #2406 --- app/assets/stylesheets/pages/projects.scss | 13 +++++++++++++ app/helpers/icons_helper.rb | 2 +- app/views/projects/forks/index.html.haml | 3 ++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 7c3587622c4..a594cc43db2 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -579,3 +579,16 @@ pre.light-well { color: #E62958; margin-top: 2px; } + +/* + * Forks list rendered on Project's forks page + */ + +.private-forks-notice .private-fork-icon { + i.fa-circle { + color: #2AA056; + } + i.fa-lock { + color: #FFFFFF; + } +} diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 5724d3aabec..84c6d0883b0 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -7,7 +7,7 @@ module IconsHelper # font-awesome-rails gem, but should we ever use a different icon pack in the # future we won't have to change hundreds of method calls. def icon(names, options = {}) - fa_icon(names, options) + options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options) end def spinner(text = nil, visible = false) diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index 10ab5c44af1..70f1392a878 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -51,7 +51,8 @@ forks: true, show_last_commit_as_description: true - if protected_count > 0 - %ul.projects-list + %ul.projects-list.private-forks-notice %li.project-row + = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon') %strong= pluralize(protected_count, 'private fork') %span you have no access to. -- cgit v1.2.1 From 99679bc6a09f9bad529bc1e8d6ff365f8e7c0fc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Fri, 15 Jan 2016 12:35:25 -0500 Subject: Add specs for Forks listing. --- features/project/fork.feature | 15 +++++++++++++++ features/steps/project/fork.rb | 25 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/features/project/fork.feature b/features/project/fork.feature index 37cd53ee977..12695204e47 100644 --- a/features/project/fork.feature +++ b/features/project/fork.feature @@ -25,3 +25,18 @@ Feature: Project Fork Then I should see "New merge request" And I click link "New merge request" Then I should see the new merge request page for my namespace + + Scenario: Viewing forks of a Project + Given I click link "Fork" + When I fork to my namespace + And I visit the forks page of the "Shop" project + Then I should see my fork on the list + + Scenario: Viewing private forks of a Project + Given There is an existent fork of the "Shop" project + And I click link "Fork" + When I fork to my namespace + And I visit the forks page of the "Shop" project + Then I should see my fork on the list + And I should not see the other fork listed + And I should see a private fork notice diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index e98bd51ca89..5810276ced3 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -49,4 +49,29 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps step 'I should see the new merge request page for my namespace' do current_path.should have_content(/#{current_user.namespace.name}/i) end + + step 'I visit the forks page of the "Shop" project' do + @project = Project.where(name: 'Shop').last + visit namespace_project_forks_path(@project.namespace, @project) + end + + step 'I should see my fork on the list' do + page.within('.projects-list-holder') do + project = @user.fork_of(@project) + expect(page).to have_content("#{project.namespace.human_name} / #{project.name}") + end + end + + step 'There is an existent fork of the "Shop" project' do + user = create(:user, name: 'Mike') + @forked_project = Projects::ForkService.new(@project, user).execute + end + + step 'I should not see the other fork listed' do + expect(page).not_to have_content("#{@forked_project.namespace.human_name} / #{@forked_project.name}") + end + + step 'I should see a private fork notice' do + expect(page).to have_content("1 private fork") + end end -- cgit v1.2.1 From ed4934dbca5a12001c13d804dd25775274dec8e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Fri, 15 Jan 2016 18:29:29 -0500 Subject: Fix broken spec. --- features/steps/project/forked_merge_requests.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index cbdce78dc0c..7e4425ff662 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -43,7 +43,9 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps expect(page).to have_css("h3.page-title", text: "New Merge Request") - fill_in "merge_request_title", with: "Merge Request On Forked Project" + page.within 'form#new_merge_request' do + fill_in "merge_request_title", with: "Merge Request On Forked Project" + end end step 'I submit the merge request' do -- cgit v1.2.1 From 91bd0bf99e0e25351c19bec28f9e5264a2fc469d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Mon, 18 Jan 2016 12:46:23 -0500 Subject: Some CSS and sorting fixes. --- app/assets/stylesheets/pages/projects.scss | 43 +++++++++++++++++----------- app/controllers/projects/forks_controller.rb | 4 +-- app/views/projects/forks/index.html.haml | 2 +- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index a594cc43db2..0e2fccb80b4 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -391,22 +391,6 @@ pre.light-well { padding-bottom: 0; margin-bottom: 0px; - &.fork-search-form { - margin: 0; - margin-top: -$gl-padding; - padding-bottom: 0; - width: 540px; - - input { - width: calc(100% - 277px); - } - - .fork_link { - float: right; - margin-left: $gl-padding; - } - } - input { display: inline-block; width: calc(100% - 151px); @@ -414,6 +398,7 @@ pre.light-well { .btn { display: inline-block; + width: 135px; } } @@ -584,6 +569,32 @@ pre.light-well { * Forks list rendered on Project's forks page */ +.projects-search-form { + &.fork-search-form { + margin: 0; + margin-top: -$gl-padding; + padding-bottom: 0; + width: 540px; + + input { + width: calc(100% - 300px); + } + + button.sort-forks { + width: 160px; + } + + .fork_link { + float: right; + margin-left: $gl-padding; + + a.btn-new { + width: 110px; + } + } + } +} + .private-forks-notice .private-fork-icon { i.fa-circle { color: #2AA056; diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb index f6fabba26f9..e61e01c4a59 100644 --- a/app/controllers/projects/forks_controller.rb +++ b/app/controllers/projects/forks_controller.rb @@ -4,8 +4,8 @@ class Projects::ForksController < Projects::ApplicationController before_action :authorize_download_code! def index - @all_forks = project.forks.includes(:creator) - @all_forks = @all_forks.sort(params[:sort]) if params[:sort] + @sort = params[:sort] || 'id_desc' + @all_forks = project.forks.includes(:creator).order_by(@sort) @public_forks, @protected_forks = @all_forks.partition do |project| can?(current_user, :read_project, project) diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index 70f1392a878..ae866dedfbd 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -11,7 +11,7 @@ spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' } .dropdown.inline.prepend-left-10 - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle.btn.sort-forks{type: 'button', 'data-toggle' => 'dropdown'} %span.light sort: - if @sort.present? = sort_options_hash[@sort] -- cgit v1.2.1 From a091483b7a70f0f94bea09c8884741e636031c41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Mon, 18 Jan 2016 17:27:50 -0500 Subject: Some fixes from last code review. --- app/assets/stylesheets/pages/projects.scss | 2 +- app/views/layouts/nav/_project.html.haml | 2 +- app/views/projects/forks/index.html.haml | 2 +- app/views/shared/projects/_project.html.haml | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 0e2fccb80b4..0643b03ed26 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -584,7 +584,7 @@ pre.light-well { width: 160px; } - .fork_link { + .fork-link { float: right; margin-left: $gl-padding; diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 292faf0e7eb..319974e12c5 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -100,7 +100,7 @@ - if project_nav_tab? :forks = nav_link(controller: :forks, action: :index) do - = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'shortcuts-wiki' do + = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks' do = icon('code-fork fw') %span Forks diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index ae866dedfbd..a5ca641558e 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -30,7 +30,7 @@ = link_to page_filter_path(sort: sort_value_oldest_updated, without: excluded_filters) do = sort_title_oldest_updated - .fork_link.inline + .fork-link.inline - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'pull-right btn btn-new' do = icon('code-fork fw') diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index c1dcdb9f0b2..2aeeed63c95 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -33,11 +33,11 @@   - if forks %span - %i.fa.fa-code-fork + = icon('code-fork') = project.forks_count - if stars %span - %i.fa.fa-star + = icon('star') = project.star_count - if show_last_commit_as_description .project-description -- cgit v1.2.1 From e9246148a6f5f61b8eeacb3539df80d390ce1ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 20 Jan 2016 00:07:40 -0500 Subject: Refactor some CSS rules. --- app/assets/stylesheets/pages/projects.scss | 21 ++++++++++++++++----- app/views/projects/forks/index.html.haml | 2 +- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 0643b03ed26..93aa61f8049 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -569,18 +569,28 @@ pre.light-well { * Forks list rendered on Project's forks page */ +.forks-top-block { + padding: 16px 0; +} + .projects-search-form { &.fork-search-form { margin: 0; margin-top: -$gl-padding; padding-bottom: 0; - width: 540px; input { - width: calc(100% - 300px); + /* Small devices (tablets, 768px and up) */ + @media (min-width: $screen-sm-min) { width: 180px; } + + /* Medium devices (desktops, 992px and up) */ + @media (min-width: $screen-md-min) { width: 350px; } + + /* Large devices (large desktops, 1200px and up) */ + @media (min-width: $screen-lg-min) { width: 400px; } } - button.sort-forks { + .sort-forks { width: 160px; } @@ -596,10 +606,11 @@ pre.light-well { } .private-forks-notice .private-fork-icon { - i.fa-circle { + i:nth-child(1) { color: #2AA056; } - i.fa-lock { + + i:nth-child(2) { color: #FFFFFF; } } diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index a5ca641558e..a362185210a 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -1,4 +1,4 @@ -.gray-content-block.top-block.clearfix.white +.gray-content-block.top-block.clearfix.white.forks-top-block .pull-left - public_count = @public_forks.size - protected_count = @protected_forks.size -- cgit v1.2.1 From 3250a68cad2afc8f1c2a4c53fb699d4dd9972a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 20 Jan 2016 00:12:46 -0500 Subject: Remove no longer required CSS rule. --- app/assets/stylesheets/pages/projects.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 93aa61f8049..d3d644329af 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -597,10 +597,6 @@ pre.light-well { .fork-link { float: right; margin-left: $gl-padding; - - a.btn-new { - width: 110px; - } } } } -- cgit v1.2.1 From a7f2d75c5d27c0042ca79e8a71b204c2bb6f1375 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Wed, 20 Jan 2016 00:27:32 -0500 Subject: Uses color variables instead. --- app/assets/stylesheets/pages/merge_requests.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 6c1272d5d8c..f033ff15f88 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -208,19 +208,19 @@ .disabled-profile { width: 40px; height: 40px; - background: #CCC; + background: $border-gray-dark; border-radius: 20px; display: inline-block; margin-right: 10px; } .disabled-comment { - background: #F3F3F3; + background: $gray-light; display: inline-block; vertical-align: top; height: 200px; border-radius: 4px; - border: 1px solid #CCC; + border: 1px solid $border-gray-normal; padding-top: 90px; text-align: center; right: 20px; -- cgit v1.2.1 From 106939fa2af87f644a4cad618d1f177b37675b5f Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Wed, 20 Jan 2016 01:52:01 -0500 Subject: Fixes test to not search whole page for content. --- features/steps/project/merge_requests.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index be993d11093..28f87a9bea0 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -41,7 +41,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I should not see "master" branch' do - expect(page).not_to have_content "master" + expect(find('.merge-request-info')).not_to have_content "master" end step 'I should see "other_branch" branch' do -- cgit v1.2.1 From f1b4f20c75d0a97c0b1f2efe6366c586a5236094 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Wed, 20 Jan 2016 02:09:19 -0500 Subject: Fix HTML so code passes Code was failing because branch was named `'test'` with quotes which was escaping the javascript I was using. --- app/views/layouts/header/_default.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index b83b5b1c9b0..d1425cd0e88 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -39,8 +39,8 @@ = render 'shared/outdated_browser' -if @project && !@project.empty_repo? && @ref :javascript - var findFileURL = '#{namespace_project_find_file_path(@project.namespace, @project, @ref)}'; + var findFileURL = "#{namespace_project_find_file_path(@project.namespace, @project, @ref)}"; -elsif @project && !@project.empty_repo? :javascript - var findFileURL = '#{namespace_project_find_file_path(@project.namespace, @project, @project.repository.root_ref)}'; \ No newline at end of file + var findFileURL = "#{namespace_project_find_file_path(@project.namespace, @project, @project.repository.root_ref)}"; \ No newline at end of file -- cgit v1.2.1 From 6d09338f8819b9772c56e5e4b509483baca72b78 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 20 Jan 2016 08:50:48 +0100 Subject: Remove `Compressed to` column from build artifacts browser See #10982. --- app/views/projects/artifacts/_tree_directory.html.haml | 1 - app/views/projects/artifacts/_tree_file.html.haml | 2 -- app/views/projects/artifacts/browse.html.haml | 1 - 3 files changed, 4 deletions(-) diff --git a/app/views/projects/artifacts/_tree_directory.html.haml b/app/views/projects/artifacts/_tree_directory.html.haml index e4b7979949c..def493c56f5 100644 --- a/app/views/projects/artifacts/_tree_directory.html.haml +++ b/app/views/projects/artifacts/_tree_directory.html.haml @@ -6,4 +6,3 @@ %span.str-truncated = link_to directory.name, path_to_directory %td - %td diff --git a/app/views/projects/artifacts/_tree_file.html.haml b/app/views/projects/artifacts/_tree_file.html.haml index 3dfc09cc495..36fb4c998c9 100644 --- a/app/views/projects/artifacts/_tree_file.html.haml +++ b/app/views/projects/artifacts/_tree_file.html.haml @@ -7,5 +7,3 @@ = link_to file.name, path_to_file %td = number_to_human_size(file.metadata[:size], precision: 2) - %td - = number_to_human_size(file.metadata[:zipped], precision: 2) diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index b70c776a2b2..d3c969cc035 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -15,7 +15,6 @@ %tr %th Name %th Size - %th Compressed to = render partial: 'tree_directory', collection: @entry.directories(parent: true), as: :directory = render partial: 'tree_file', collection: @entry.files, as: :file -- cgit v1.2.1 From 7a609858f063c1089f3a6ec05f9f87a01efa909a Mon Sep 17 00:00:00 2001 From: Jeroen Nijhof Date: Wed, 20 Jan 2016 09:48:38 +0100 Subject: Use sentry in env production only --- Gemfile | 6 +++--- app/views/admin/application_settings/_form.html.haml | 4 +++- config/initializers/sentry.rb | 20 +++++++++++--------- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/Gemfile b/Gemfile index e8fa75e0974..1a91e9cc3e8 100644 --- a/Gemfile +++ b/Gemfile @@ -293,6 +293,9 @@ end group :production do gem "gitlab_meta", '7.0' + + # Sentry integration + gem 'sentry-raven' end gem "newrelic_rpm", '~> 3.9.4.245' @@ -314,6 +317,3 @@ gem 'oauth2', '~> 1.0.0' # Soft deletion gem "paranoia", "~> 2.0" - -# Sentry integration -gem 'sentry-raven' diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 35af5cf620a..f5a87933fde 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -242,7 +242,9 @@ = f.label :sentry_enabled do = f.check_box :sentry_enabled Enable Sentry - %span.help-block#sentry_help_block Sentry is an error reporting and logging tool + .help-block + Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: + %a{ href: 'https://getsentry.com', target: 'blank' } https://getsentry.com .form-group = f.label :sentry_dsn, 'Sentry DSN', class: 'control-label col-sm-2' diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index 3ef46291981..d0630b9fa07 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -3,15 +3,17 @@ require 'gitlab/current_settings' include Gitlab::CurrentSettings -# allow it to fail: it may do so when create_from_defaults is executed before migrations are actually done -begin - sentry_enabled = current_application_settings.sentry_enabled -rescue - sentry_enabled = false -end +if Rails.env.production? + # allow it to fail: it may do so when create_from_defaults is executed before migrations are actually done + begin + sentry_enabled = current_application_settings.sentry_enabled + rescue + sentry_enabled = false + end -if sentry_enabled - Raven.configure do |config| - config.dsn = current_application_settings.sentry_dsn + if sentry_enabled + Raven.configure do |config| + config.dsn = current_application_settings.sentry_dsn + end end end -- cgit v1.2.1 From 9d756f3d8be74103fff0c119e2e1e0701ec93d54 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 20 Jan 2016 10:33:59 +0100 Subject: Add document on restarting GitLab [ci skip] --- doc/README.md | 1 + doc/administration/restart_gitlab.md | 138 +++++++++++++++++++++++++++++++++++ doc/development/doc_styleguide.md | 11 +++ 3 files changed, 150 insertions(+) create mode 100644 doc/administration/restart_gitlab.md diff --git a/doc/README.md b/doc/README.md index 7d4f84857e0..f6dbfedafca 100644 --- a/doc/README.md +++ b/doc/README.md @@ -53,6 +53,7 @@ - [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when web hooks aren't enough. - [Install](install/README.md) Requirements, directory structures and installation from source. +- [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components - [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, LDAP and Twitter. - [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages. - [Libravatar](customization/libravatar.md) Use Libravatar for user avatars. diff --git a/doc/administration/restart_gitlab.md b/doc/administration/restart_gitlab.md new file mode 100644 index 00000000000..bb4fe9f414e --- /dev/null +++ b/doc/administration/restart_gitlab.md @@ -0,0 +1,138 @@ +# How to restart GitLab + +Depending on how you installed GitLab, there are different methods to restart +its service(s). + +If you want the TL;DR versions, jump to: + +- [Omnibus GitLab restart](#omnibus-gitlab-restart) +- [Omnibus GitLab reconfigure](#omnibus-gitlab-reconfigure) +- [Source installation restart](#installations-from-source) + +## Omnibus installations + +If you have used the [Omnibus packages][omnibus-dl] to install GitLab, then +you should already have `gitlab-ctl` in your `PATH`. To find out, run: + +```bash +which gitlab-ctl +``` + +The output should be: `/usr/bin/gitlab-ctl`. + +`gitlab-ctl` interacts with the Omnibus packages and can be used to restart the +GitLab Rails application (Unicorn) as well as the other components, like: + +- GitLab Workhorse +- Sidekiq +- PostgreSQL (if you are using the bundled one) +- NGINX (if you are using the bundled one) +- Redis (if you are using the bundled one) +- [Mailroom][] +- Logrotate. + +### Omnibus GitLab restart + +When you are asked to _restart GitLab_, you need to run the following command: + +```bash +sudo gitlab-ctl restart +``` + +The output should be similar to this: + +``` +ok: run: gitlab-workhorse: (pid 11291) 1s +ok: run: logrotate: (pid 11299) 0s +ok: run: mailroom: (pid 11306) 0s +ok: run: nginx: (pid 11309) 0s +ok: run: postgresql: (pid 11316) 1s +ok: run: redis: (pid 11325) 0s +ok: run: sidekiq: (pid 11331) 1s +ok: run: unicorn: (pid 11338) 0s +``` + +To restart a component separately, you can append its service name to the +`restart` command. For example, to restart **only** NGINX you would run: + +```bash +sudo gitlab-ctl restart nginx +``` + +To check the status of GitLab services, run: + +```bash +sudo gitlab-ctl status +``` + +Notice that all services say `ok: run`. If you get any weird results, like the +unicorn service not starting, you may need to +[reconfigure GitLab](#omnibus-gitlab-reconfigure) instead. + +### Omnibus GitLab reconfigure + +There may be times where you will be asked to _reconfigure_ GitLab. Remember +that this method applies only for the Omnibus packages. + +Reconfigure Omnibus GitLab with: + +```bash +sudo gitlab-ctl reconfigure +``` + +Reconfiguring GitLab should occur in the event that something in its +configuration (`/etc/gitlab/gitlab.rb`) has changed. + +When you run this command, [Chef], the underlying configuration management +application that powers Omnibus GitLab, will make sure that all directories, +permissions, services, etc., are in place and in the same shape that they were +initially shipped. This is where the _idempotency_ buzz-word you've been reading +here and there fits. + +## Installations from source + +If you have followed the official installation guide to [install GitLab from +source][install], run the following command to restart GitLab: + +``` +sudo service gitlab restart +``` + +The output should be similar to this: + +``` +Shutting down GitLab Unicorn +Shutting down GitLab Sidekiq +Shutting down GitLab Workhorse +Shutting down GitLab MailRoom +... +GitLab is not running. +Starting GitLab Unicorn +Starting GitLab Sidekiq +Starting GitLab Workhorse +Starting GitLab MailRoom +... +The GitLab Unicorn web server with pid 28059 is running. +The GitLab Sidekiq job dispatcher with pid 28176 is running. +The GitLab Workhorse with pid 28122 is running. +The GitLab MailRoom email processor with pid 28114 is running. +GitLab and all its components are up and running. +``` + +This should restart Unicorn, Sidekiq, GitLab Workhorse and [Mailroom][] +(if enabled). The init service file that does all the magic can be found in +[`lib/support/init.d/gitlab`][src-service]. + +--- + +If you are using other init systems, like systemd, you can check the +[GitLab Recipes][gl-recipes] repository for some unofficial services. These are +**not** officially supported so use at your own risk. + + +[omnibus-dl]: https://about.gitlab.com/downloads/ "Download the Omnibus packages" +[install]: ../install/installation.md "Documentation to install GitLab from source" +[mailroom]: ../incoming_email/README.md "Used for replying by email in GitLab issues and merge requests" +[chef]: https://www.chef.io/chef/ "Chef official website" +[src-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/init.d/gitlab "GitLab init service file" +[gl-recipes]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/init "GitLab Recipes repository" diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 0bd32b78201..0539bc2df21 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -103,6 +103,17 @@ Inside the document: `_**Note:** This feature was introduced in GitLab EE 8.3_`. Otherwise, leave this mention out +## References + +- There are many cases that a restart/reconfigure of GitLab is required. To + avoid duplication, link to the special document that can be found in + `doc/administration/restart_gitlab.md`. Usually the text will read like: + + ``` + Save the file and [reconfigure GitLab](../administration/restart_gitlab.md) for the changes to take effect. + ``` + Replace `reconfigure` with `restart` where appropriate. + ## API Here is a list of must-have items. Use them in the exact order that appears -- cgit v1.2.1 From 086cbca6bf9d6fdad862a19b7fe468a315b05803 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 20 Jan 2016 10:44:26 +0100 Subject: Link to restart_gitlab.md and some more fixes [ci skip] --- doc/development/doc_styleguide.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 0539bc2df21..caaa4032db2 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -105,13 +105,19 @@ Inside the document: ## References -- There are many cases that a restart/reconfigure of GitLab is required. To +- **GitLab Restart:** + There are many cases that a restart/reconfigure of GitLab is required. To avoid duplication, link to the special document that can be found in - `doc/administration/restart_gitlab.md`. Usually the text will read like: + [`doc/administration/restart_gitlab.md`][doc-restart]. Usually the text will + read like: ``` - Save the file and [reconfigure GitLab](../administration/restart_gitlab.md) for the changes to take effect. + Save the file and [reconfigure GitLab](../administration/restart_gitlab.md) + for the changes to take effect. ``` + If the document you are editing resides in a place other than the GitLab CE/EE + `doc/` directory, instead of the relative link, use the full path: + `http://doc.gitlab.com/ce/administration/restart_gitlab.html`. Replace `reconfigure` with `restart` where appropriate. ## API @@ -240,3 +246,4 @@ curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -d "restricted_signup_domai [cURL]: http://curl.haxx.se/ "cURL website" [single spaces]: http://www.slate.com/articles/technology/technology/2011/01/space_invaders.html [gfm]: http://doc.gitlab.com/ce/markdown/markdown.html#newlines "GitLab flavored markdown documentation" +[doc-restart]: ../administration/restart_gitlab.md "GitLab restart documentation" -- cgit v1.2.1 From 36aa3e617001e3b84ed4b5dd9025261cd531344c Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 20 Jan 2016 10:56:41 +0100 Subject: Add basic documentation for build artifacts --- doc/README.md | 1 + doc/ci/README.md | 1 + doc/ci/artifacts/README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 doc/ci/artifacts/README.md diff --git a/doc/README.md b/doc/README.md index 7d4f84857e0..d75c1fbfe79 100644 --- a/doc/README.md +++ b/doc/README.md @@ -30,6 +30,7 @@ - [User permissions](ci/permissions/README.md) - [API](ci/api/README.md) - [Triggering builds through the API](ci/triggers/README.md) +- [Build artifacts](ci/artifacts/README.md) ### CI Languages diff --git a/doc/ci/README.md b/doc/ci/README.md index 4cdd2e1ad33..19d08721467 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -12,6 +12,7 @@ * [Using Variables](variables/README.md) * [Using SSH keys](ssh_keys/README.md) * [Triggering builds through the API](triggers/README.md) +* [Build artifacts](artifacts/README.md) ### Languages diff --git a/doc/ci/artifacts/README.md b/doc/ci/artifacts/README.md new file mode 100644 index 00000000000..1166304f33c --- /dev/null +++ b/doc/ci/artifacts/README.md @@ -0,0 +1,50 @@ +# Build artifacts + +Since version 8.2 of GitLab and version 0.7.0 of GitLab Runner, build artifacts +created by GitLab Runner are uploaded to GitLab, and then you can download +artifacts archive using GitLab UI. + +Since version 8.4 of GitLab and version 1.0 of GitLab Runner artifacts are +compressed using ZIP format and it is possible to browse content of such an +archive using GitLab UI, and then download a single file from inside it. + +## Artifacts in .gitlab-ci.yml + +Please look into `.gitlab-ci.yml` [documentation](../yaml/README.md). + +## Artifacts archive format + +Prior to version 8.4 of GitLab and 1.0 of GitLab Runner, build artifacts were +compressed using `tar.gz` format. + +Since then, we use a ZIP format. + +## How build artifacts are stored + +After a successful build, GitLab Runner uploads an archive containing build +artifacts to GitLab. This archive is not extracted after that, so its save a +storage space. + +## How do we access content of an artifacts archive + +When GitLab receives an artifacts archive, archive metadata file is being +generated. Metadata file describes all entries that are located in artifacts +archive. This file is in a binary format, with additional GZIP compression. + +It is possible then to browse artifacts using GitLab UI and artifacts browser. + +TODO IMG + +GitLab does not extract artifacts archive to make it possible to browse it. We +use artifacts metadata file instead that contains are relevant information. +This is especially important when there is a lot of artifacts, or an archive is +a very large file. + +## How do we make files downloadable + +When user clicks a regular file, then download of this particular file starts. +GitLab does not extract entire artifacts archive to send a single file to user. + +Instead of extracting entire file, only one file is being extracted. It is not +necessary to extract large archive, just to download a small file that is +inside. -- cgit v1.2.1 From 21fab4f41317fe3c7acc7f9395cb1222e4a41074 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 20 Jan 2016 11:49:42 +0100 Subject: Use env variables for Unicorn memory limits This makes it easier for users to use their own limits based on their server configuration. --- config.ru | 5 ++++- doc/administration/environment_variables.md | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/config.ru b/config.ru index a2525c81361..1b258e0bd5e 100644 --- a/config.ru +++ b/config.ru @@ -7,8 +7,11 @@ if defined?(Unicorn) # Unicorn self-process killer require 'unicorn/worker_killer' + min = (ENV['GITLAB_UNICORN_MEMORY_MIN'] || 200 * 1 << 20).to_i + max = (ENV['GITLAB_UNICORN_MEMORY_MAX'] || 250 * 1 << 20).to_i + # Max memory size (RSS) per worker - use Unicorn::WorkerKiller::Oom, (200 * (1 << 20)), (250 * (1 << 20)) + use Unicorn::WorkerKiller::Oom, min, max end end diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md index 1eb3a74d304..42a27dcf6d6 100644 --- a/doc/administration/environment_variables.md +++ b/doc/administration/environment_variables.md @@ -17,6 +17,8 @@ DATABASE_URL | url | For example: postgresql://localhost/blog_development?pool=5 GITLAB_EMAIL_FROM | email | Email address used in the "From" field in mails sent by GitLab GITLAB_EMAIL_DISPLAY_NAME | string | Name used in the "From" field in mails sent by GitLab GITLAB_EMAIL_REPLY_TO | email | Email address used in the "Reply-To" field in mails sent by GitLab +GITLAB_UNICORN_MEMORY_MIN | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer +GITLAB_UNICORN_MEMORY_MAX | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer ## Complete database variables -- cgit v1.2.1 From 89c65a3789ab51d58633841120c33f570064f242 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 20 Jan 2016 11:52:01 +0100 Subject: Increase Unicorn memory limits to 300-350 Using this limit on GitLab.com it appears we're able to reduce response timings by about 620 milliseconds compared to the previous limit. See gitlab-org/gitlab-ce!2421 for more information. --- config.ru | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config.ru b/config.ru index 1b258e0bd5e..065ce59932f 100644 --- a/config.ru +++ b/config.ru @@ -7,8 +7,8 @@ if defined?(Unicorn) # Unicorn self-process killer require 'unicorn/worker_killer' - min = (ENV['GITLAB_UNICORN_MEMORY_MIN'] || 200 * 1 << 20).to_i - max = (ENV['GITLAB_UNICORN_MEMORY_MAX'] || 250 * 1 << 20).to_i + min = (ENV['GITLAB_UNICORN_MEMORY_MIN'] || 300 * 1 << 20).to_i + max = (ENV['GITLAB_UNICORN_MEMORY_MAX'] || 350 * 1 << 20).to_i # Max memory size (RSS) per worker use Unicorn::WorkerKiller::Oom, min, max -- cgit v1.2.1 From 6f32459f2c0ebc0aa10c728e5562375db9cf4c52 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 20 Jan 2016 12:05:03 +0100 Subject: Renamed "Metrics" to "Performance Monitoring" --- doc/README.md | 2 +- doc/integration/README.md | 2 +- doc/integration/metrics/gitlab_configuration.md | 6 +++--- doc/integration/metrics/influxdb_configuration.md | 2 +- doc/integration/metrics/influxdb_schema.md | 2 +- doc/integration/metrics/introduction.md | 10 +++++----- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/doc/README.md b/doc/README.md index bee9ab05906..05c0d6fa0fe 100644 --- a/doc/README.md +++ b/doc/README.md @@ -67,7 +67,7 @@ - [Reply by email](incoming_email/README.md) Allow users to comment on issues and merge requests by replying to notification emails. - [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE. - [Git LFS configuration](workflow/lfs/lfs_administration.md) -- [GitLab Metrics](integration/metrics/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics +- [GitLab Performance Monitoring](integration/metrics/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics ## Contributor documentation diff --git a/doc/integration/README.md b/doc/integration/README.md index 5ba0d0fb245..297e4f6e7bf 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -15,7 +15,7 @@ See the documentation below for details on how to configure these services. - [OAuth2 provider](oauth_provider.md) OAuth2 application creation - [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages - [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users -- [GitLab Metrics](metrics/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics +- [GitLab Performance Monitoring](metrics/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics GitLab Enterprise Edition contains [advanced Jenkins support][jenkins]. diff --git a/doc/integration/metrics/gitlab_configuration.md b/doc/integration/metrics/gitlab_configuration.md index 019db1ad02c..b856e7935a3 100644 --- a/doc/integration/metrics/gitlab_configuration.md +++ b/doc/integration/metrics/gitlab_configuration.md @@ -1,6 +1,6 @@ # GitLab Configuration -GitLab Metrics is disabled by default. To enable it and change any of its +GitLab Performance Monitoring is disabled by default. To enable it and change any of its settings, navigate to the Admin area in **Settings > Metrics** (`/admin/application_settings`). @@ -10,7 +10,7 @@ changes. --- -![GitLab Metrics Admin Settings](img/metrics_gitlab_configuration_settings.png) +![GitLab Performance Monitoring Admin Settings](img/metrics_gitlab_configuration_settings.png) --- @@ -34,6 +34,6 @@ have been performed. Read more on: -- [Introduction to GitLab Metrics](introduction.md) +- [Introduction to GitLab Performance Monitoring](introduction.md) - [InfluxDB Configuration](influxdb_configuration.md) - [InfluxDB Schema](influxdb_schema.md) diff --git a/doc/integration/metrics/influxdb_configuration.md b/doc/integration/metrics/influxdb_configuration.md index 13227f1e889..df5e4af7fad 100644 --- a/doc/integration/metrics/influxdb_configuration.md +++ b/doc/integration/metrics/influxdb_configuration.md @@ -178,7 +178,7 @@ That's it! Now your GitLab instance should send data to InfluxDB. Read more on: -- [Introduction to GitLab Metrics](introduction.md) +- [Introduction to GitLab Performance Monitoring](introduction.md) - [GitLab Configuration](gitlab_configuration.md) - [InfluxDB Schema](influxdb_schema.md) diff --git a/doc/integration/metrics/influxdb_schema.md b/doc/integration/metrics/influxdb_schema.md index a9ef0b446c8..a5a8aebd2d1 100644 --- a/doc/integration/metrics/influxdb_schema.md +++ b/doc/integration/metrics/influxdb_schema.md @@ -82,6 +82,6 @@ view. Read more on: -- [Introduction to GitLab Metrics](introduction.md) +- [Introduction to GitLab Performance Monitoring](introduction.md) - [GitLab Configuration](gitlab_configuration.md) - [InfluxDB Configuration](influxdb_configuration.md) diff --git a/doc/integration/metrics/introduction.md b/doc/integration/metrics/introduction.md index bf388aa2bba..f2460d31302 100644 --- a/doc/integration/metrics/introduction.md +++ b/doc/integration/metrics/introduction.md @@ -1,19 +1,19 @@ -# GitLab Metrics +# GitLab Performance Monitoring GitLab comes with its own application performance measuring system as of GitLab -8.4, simply called "GitLab Metrics". GitLab Metrics is available in both the +8.4, simply called "GitLab Performance Monitoring". GitLab Performance Monitoring is available in both the Community and Enterprise editions. Apart from this introduction, you are advised to read through the following -documents in order to understand and properly configure GitLab Metrics: +documents in order to understand and properly configure GitLab Performance Monitoring: - [GitLab Configuration](gitlab_configuration.md) - [InfluxDB Configuration](influxdb_configuration.md) - [InfluxDB Schema](influxdb_schema.md) -## Introduction to GitLab Metrics +## Introduction to GitLab Performance Monitoring -GitLab Metrics makes it possible to measure a wide variety of statistics +GitLab Performance Monitoring makes it possible to measure a wide variety of statistics including (but not limited to): - The time it took to complete a transaction (a web request or Sidekiq job). -- cgit v1.2.1 From 82a3cfc151c4c96935387be3e94f703beb449b25 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Wed, 20 Jan 2016 01:24:17 -0200 Subject: [ci skip] Update CHANGELOG --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 2e0eee52a59..edb4e6a2350 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.5.0 (unreleased) - Add "visibility" flag to GET /projects api endpoint + - Don't vendor minified JS v 8.4.0 (unreleased) - Allow LDAP users to change their email if it was not set by the LDAP server @@ -71,6 +72,7 @@ v 8.4.0 (unreleased) - Expose button to CI Lint tool on project builds page - Fix: Creator should be added as a master of the project on creation - Added X-GitLab-... headers to emails from CI and Email On Push services (Anton Baklanov) + - Autofill referring url in message box when reporting user abuse. (Josh Frye) v 8.3.4 - Use gitlab-workhorse 0.5.4 (fixes API routing bug) -- cgit v1.2.1 From d14ed863f7723b7de201a09861deac8787fca25d Mon Sep 17 00:00:00 2001 From: Henning Hoefer Date: Wed, 20 Jan 2016 12:47:51 +0000 Subject: Add note about minimum GitLab CI version --- doc/ci/variables/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index b99ea25a3fe..862cacda586 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -56,7 +56,7 @@ export CI_SERVER_VERSION="" ``` ### YAML-defined variables -**This feature requires GitLab Runner 0.5.0 or higher** +**This feature requires GitLab Runner 0.5.0 or higher and GitLab CI 7.14 or higher ** GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build environment. The variables are stored in repository and are meant to store non-sensitive project configuration, ie. RAILS_ENV or DATABASE_URL. -- cgit v1.2.1 From 701513dcc7afb403372bc738642a9a52e9be5983 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 20 Jan 2016 14:51:56 +0100 Subject: Move parallel diff logic to separate class --- app/helpers/diff_helper.rb | 104 ------------------ app/views/projects/diffs/_parallel_view.html.haml | 6 +- app/views/projects/diffs/_text_file.html.haml | 2 +- .../projects/notes/discussions/_diff.html.haml | 2 +- lib/gitlab/diff/file.rb | 4 + lib/gitlab/diff/parallel_diff.rb | 117 +++++++++++++++++++++ spec/helpers/diff_helper_spec.rb | 18 ---- spec/lib/gitlab/diff/parallel_diff_spec.rb | 22 ++++ 8 files changed, 148 insertions(+), 127 deletions(-) create mode 100644 lib/gitlab/diff/parallel_diff.rb create mode 100644 spec/lib/gitlab/diff/parallel_diff_spec.rb diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 425a8ced549..e524f6da9c8 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -39,110 +39,6 @@ module DiffHelper end end - def generate_line_code(file_path, line) - Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) - end - - def parallel_diff(diff_file, index) - lines = [] - skip_next = false - - diff_file.highlighted_diff_lines.each do |line| - full_line = line.text - type = line.type - line_code = generate_line_code(diff_file.file_path, line) - line_new = line.new_pos - line_old = line.old_pos - - next_line = diff_file.next_line(line.index) - - if next_line - next_line_code = generate_line_code(diff_file.file_path, next_line) - next_type = next_line.type - next_line = next_line.text - end - - case type - when 'match', nil - # line in the right panel is the same as in the left one - lines << { - left: { - type: type, - number: line_old, - text: full_line, - line_code: line_code, - }, - right: { - type: type, - number: line_new, - text: full_line, - line_code: line_code - } - } - when 'old' - case next_type - when 'new' - # Left side has text removed, right side has text added - lines << { - left: { - type: type, - number: line_old, - text: full_line, - line_code: line_code, - }, - right: { - type: next_type, - number: line_new, - text: next_line, - line_code: next_line_code - } - } - skip_next = true - when 'old', nil - # Left side has text removed, right side doesn't have any change - # No next line code, no new line number, no new line text - lines << { - left: { - type: type, - number: line_old, - text: full_line, - line_code: line_code, - }, - right: { - type: next_type, - number: nil, - text: "", - line_code: nil - } - } - end - when 'new' - if skip_next - # Change has been already included in previous line so no need to do it again - skip_next = false - next - else - # Change is only on the right side, left side has no change - lines << { - left: { - type: nil, - number: nil, - text: "", - line_code: line_code, - }, - right: { - type: type, - number: line_new, - text: full_line, - line_code: line_code - } - } - end - end - end - lines - end - def unfold_bottom_class(bottom) (bottom) ? 'js-unfold-bottom' : '' end diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 7b9cecbc3da..986d30d36a8 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -1,7 +1,7 @@ / Side-by-side diff view %div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight %table - - parallel_diff(diff_file, index).each do |line| + - diff_file.parallel_diff_lines.each do |line| - left = line[:left] - right = line[:right] %tr.line_holder.parallel @@ -13,7 +13,7 @@ = link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code] - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(left[:line_code], 'old') - %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]}", data: { "line_code" => left[:line_code] }}= diff_line_content(left[:text]) + %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text]) - if right[:type] == 'new' - new_line_class = 'new' @@ -26,7 +26,7 @@ = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(right[:line_code], 'new') - %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", data: { "line_code" => new_line_code }}= diff_line_content(right[:text]) + %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", data: { line_code: new_line_code }}= diff_line_content(right[:text]) - if @reply_allowed - comments_left, comments_right = organize_comments(left[:type], right[:type], left[:line_code], right[:line_code]) diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index 6761155dcf9..310bbd4efea 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -22,7 +22,7 @@ = link_to_new_diff_note(line_code) %td.new_line{data: {linenumber: line.new_pos}} = link_to raw(type == "old" ? " " : line.new_pos), "##{line_code}", id: line_code - %td.line_content{class: "noteable_line #{type} #{line_code}", data: { "line_code" => line_code }}= diff_line_content(line.text) + %td.line_content{class: "noteable_line #{type} #{line_code}", data: { line_code: line_code }}= diff_line_content(line.text) - if @reply_allowed - comments = @line_notes.select { |n| n.line_code == line_code && n.active? }.sort_by(&:created_at) diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml index 97347a9f67f..46962b184ce 100644 --- a/app/views/projects/notes/discussions/_diff.html.haml +++ b/app/views/projects/notes/discussions/_diff.html.haml @@ -24,7 +24,7 @@ = raw(type == "new" ? " " : line.old_pos) %td.new_line = raw(type == "old" ? " " : line.new_pos) - %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= diff_line_content(line.text) + %td.line_content{class: "noteable_line #{type} #{line_code}", line_code: line_code}= diff_line_content(line.text) - if line_code == note.line_code = render "projects/notes/diff_notes_with_reply", notes: discussion_notes diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index a6a7fc8ff4c..74b1c117129 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -20,6 +20,10 @@ module Gitlab Gitlab::Diff::Highlight.new(self).highlight end + def parallel_diff_lines + Gitlab::Diff::ParallelDiff.new(self).parallelize + end + def mode_changed? !!(diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode) end diff --git a/lib/gitlab/diff/parallel_diff.rb b/lib/gitlab/diff/parallel_diff.rb new file mode 100644 index 00000000000..a0dc3da875d --- /dev/null +++ b/lib/gitlab/diff/parallel_diff.rb @@ -0,0 +1,117 @@ +module Gitlab + module Diff + class ParallelDiff + attr_accessor :diff_file + + def initialize(diff_file) + @diff_file = diff_file + end + + def parallelize + lines = [] + skip_next = false + + diff_file.highlighted_diff_lines.each do |line| + full_line = line.text + type = line.type + line_code = generate_line_code(diff_file.file_path, line) + line_new = line.new_pos + line_old = line.old_pos + + next_line = diff_file.next_line(line.index) + + if next_line + next_line_code = generate_line_code(diff_file.file_path, next_line) + next_type = next_line.type + next_line = next_line.text + end + + case type + when 'match', nil + # line in the right panel is the same as in the left one + lines << { + left: { + type: type, + number: line_old, + text: full_line, + line_code: line_code, + }, + right: { + type: type, + number: line_new, + text: full_line, + line_code: line_code + } + } + when 'old' + case next_type + when 'new' + # Left side has text removed, right side has text added + lines << { + left: { + type: type, + number: line_old, + text: full_line, + line_code: line_code, + }, + right: { + type: next_type, + number: line_new, + text: next_line, + line_code: next_line_code + } + } + skip_next = true + when 'old', nil + # Left side has text removed, right side doesn't have any change + # No next line code, no new line number, no new line text + lines << { + left: { + type: type, + number: line_old, + text: full_line, + line_code: line_code, + }, + right: { + type: next_type, + number: nil, + text: "", + line_code: nil + } + } + end + when 'new' + if skip_next + # Change has been already included in previous line so no need to do it again + skip_next = false + next + else + # Change is only on the right side, left side has no change + lines << { + left: { + type: nil, + number: nil, + text: "", + line_code: line_code, + }, + right: { + type: type, + number: line_new, + text: full_line, + line_code: line_code + } + } + end + end + end + lines + end + + private + + def generate_line_code(file_path, line) + Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) + end + end + end +end diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 435ecd04fbe..955d2852cfd 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -84,20 +84,6 @@ describe DiffHelper do end end - describe 'parallel_diff' do - it 'should return an array of arrays containing the parsed diff' do - expect(parallel_diff(diff_file, 0)). - to match_array(parallel_diff_result_array) - end - end - - describe 'generate_line_code' do - it 'should generate correct line code' do - expect(generate_line_code(diff_file.file_path, diff_file.diff_lines.first)). - to eq('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6') - end - end - describe 'unfold_bottom_class' do it 'should return empty string when bottom line shouldnt be unfolded' do expect(unfold_bottom_class(false)).to eq('') @@ -135,8 +121,4 @@ describe DiffHelper do expect(diff_line_content(diff_file.diff_lines.first.text)).to be_html_safe end end - - def parallel_diff_result_array - YAML.load_file("#{Rails.root}/spec/fixtures/parallel_diff_result.yml") - end end diff --git a/spec/lib/gitlab/diff/parallel_diff_spec.rb b/spec/lib/gitlab/diff/parallel_diff_spec.rb new file mode 100644 index 00000000000..852218b4e62 --- /dev/null +++ b/spec/lib/gitlab/diff/parallel_diff_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Gitlab::Diff::ParallelDiff, lib: true do + include RepoHelpers + + let(:project) { create(:project) } + let(:repository) { project.repository } + let(:commit) { project.commit(sample_commit.id) } + let(:diffs) { commit.diffs } + let(:diff) { diffs.first } + let(:diff_refs) { [commit.parent, commit] } + let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs) } + subject { described_class.new(diff_file) } + + let(:parallel_diff_result_array) { YAML.load_file("#{Rails.root}/spec/fixtures/parallel_diff_result.yml") } + + describe '#parallelize' do + it 'should return an array of arrays containing the parsed diff' do + expect(subject.parallelize).to match_array(parallel_diff_result_array) + end + end +end -- cgit v1.2.1 From 88bd13851300b98c9d0290cfac5ad1f14673b34d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 20 Jan 2016 14:59:14 +0100 Subject: Restore helper --- app/helpers/diff_helper.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index e524f6da9c8..f500d3572e0 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -39,6 +39,10 @@ module DiffHelper end end + def generate_line_code(file_path, line) + Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) + end + def unfold_bottom_class(bottom) (bottom) ? 'js-unfold-bottom' : '' end -- cgit v1.2.1 From a010db5db243a532cb8d1c2d5ac787e90da0044f Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 20 Jan 2016 15:26:44 +0100 Subject: Properly handle HTML entities with inline diffs --- lib/gitlab/diff/inline_diff.rb | 11 ++++------- lib/gitlab/diff/inline_diff_marker.rb | 16 ++++++++++++++-- lib/gitlab/diff/parser.rb | 7 +------ spec/lib/gitlab/diff/inline_diff_marker_spec.rb | 8 ++++---- spec/lib/gitlab/diff/parser_spec.rb | 2 +- 5 files changed, 24 insertions(+), 20 deletions(-) diff --git a/lib/gitlab/diff/inline_diff.rb b/lib/gitlab/diff/inline_diff.rb index e5986fd69e2..b8a61ad6115 100644 --- a/lib/gitlab/diff/inline_diff.rb +++ b/lib/gitlab/diff/inline_diff.rb @@ -16,15 +16,12 @@ module Gitlab old_line = @lines[old_index] new_line = @lines[new_index] - suffixless_old_line = old_line[1..-1] - suffixless_new_line = new_line[1..-1] - # Skip inline diff if empty line was replaced with content - next if suffixless_old_line == "" + next if old_line[1..-1] == "" - # Add one, because this is based on the suffixless version - lcp = longest_common_prefix(suffixless_old_line, suffixless_new_line) + 1 - lcs = longest_common_suffix(suffixless_old_line, suffixless_new_line) + # Add one, because this is based on the prefixless version + lcp = longest_common_prefix(old_line[1..-1], new_line[1..-1]) + 1 + lcs = longest_common_suffix(old_line[lcp..-1], new_line[lcp..-1]) old_diff_range = lcp..(old_line.length - lcs - 1) new_diff_range = lcp..(new_line.length - lcs - 1) diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb index 8998ccba5ce..c31149d374e 100644 --- a/lib/gitlab/diff/inline_diff_marker.rb +++ b/lib/gitlab/diff/inline_diff_marker.rb @@ -12,7 +12,7 @@ module Gitlab offset = 0 line_inline_diffs.each do |inline_diff_range| # Map the inline-diff range based on the raw line to character positions in the rich line - inline_diff_positions = position_mapping[inline_diff_range] + inline_diff_positions = position_mapping[inline_diff_range].flatten # Turn the array of character positions into ranges marker_ranges = collapse_ranges(inline_diff_positions) @@ -47,7 +47,19 @@ module Gitlab rich_char = rich_line[rich_pos] end - mapping[raw_pos] = rich_pos + # multi-char HTML entities in the rich line correspond to a single character in the raw line + if rich_char == '&' + multichar_mapping = [rich_pos] + until rich_char == ';' + rich_pos += 1 + multichar_mapping << rich_pos + rich_char = rich_line[rich_pos] + end + + mapping[raw_pos] = multichar_mapping + else + mapping[raw_pos] = rich_pos + end rich_pos += 1 end diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb index 2c0a866e8ba..6c8a1fc6d6f 100644 --- a/lib/gitlab/diff/parser.rb +++ b/lib/gitlab/diff/parser.rb @@ -14,7 +14,7 @@ module Gitlab @lines.each do |line| next if filename?(line) - full_line = html_escape(line.gsub(/\n/, '')) + full_line = line.gsub(/\n/, '') if line.match(/^@@ -/) type = "match" @@ -67,11 +67,6 @@ module Gitlab nil end end - - def html_escape(str) - replacements = { '&' => '&', '>' => '>', '<' => '<', '"' => '"', "'" => ''' } - str.gsub(/[&"'><]/, replacements) - end end end end diff --git a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb index 5d6aea17509..6f3276a8b53 100644 --- a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb +++ b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb @@ -2,14 +2,14 @@ require 'spec_helper' describe Gitlab::Diff::InlineDiffMarker, lib: true do describe '#inline_diffs' do - let(:raw) { "abc def" } - let(:rich) { %{abc def} } - let(:inline_diffs) { [2..4] } + let(:raw) { "abc 'def'" } + let(:rich) { %{abc 'def'} } + let(:inline_diffs) { [2..5] } let(:subject) { Gitlab::Diff::InlineDiffMarker.new(raw, rich).mark(inline_diffs) } it 'marks the inline diffs' do - expect(subject).to eq(%{abc def}) + expect(subject).to eq(%{abc 'def'}) end end end diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb index ba577bd28e5..fe0dea77909 100644 --- a/spec/lib/gitlab/diff/parser_spec.rb +++ b/spec/lib/gitlab/diff/parser_spec.rb @@ -86,7 +86,7 @@ eos it { expect(line.type).to eq(nil) } it { expect(line.old_pos).to eq(24) } it { expect(line.new_pos).to eq(31) } - it { expect(line.text).to eq(' @cmd_output << stderr.read') } + it { expect(line.text).to eq(' @cmd_output << stderr.read') } end end end -- cgit v1.2.1 From 8536e083f7b2d7ed77ecae83774d75f68d66e0b4 Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Wed, 20 Jan 2016 13:16:34 +0100 Subject: Add IP blocking against DNSBL at sign-up --- CHANGELOG | 1 + .../admin/application_settings_controller.rb | 2 + app/controllers/registrations_controller.rb | 5 + app/models/application_setting.rb | 2 + .../admin/application_settings/_form.html.haml | 16 ++++ ...ip_blocking_settings_to_application_settings.rb | 6 ++ db/schema.rb | 4 +- lib/dnsxl_check.rb | 105 +++++++++++++++++++++ lib/gitlab/ip_check.rb | 34 +++++++ spec/lib/dnsxl_check_spec.rb | 68 +++++++++++++ 10 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb create mode 100644 lib/dnsxl_check.rb create mode 100644 lib/gitlab/ip_check.rb create mode 100644 spec/lib/dnsxl_check_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 2e0eee52a59..a17f2bfef45 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -71,6 +71,7 @@ v 8.4.0 (unreleased) - Expose button to CI Lint tool on project builds page - Fix: Creator should be added as a master of the project on creation - Added X-GitLab-... headers to emails from CI and Email On Push services (Anton Baklanov) + - Add IP check against DNSBLs at account sign-up v 8.3.4 - Use gitlab-workhorse 0.5.4 (fixes API routing bug) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 91f7d78bd73..6df8e8e4cd8 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -74,6 +74,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :metrics_timeout, :metrics_method_call_threshold, :metrics_sample_interval, + :ip_blocking_enabled, + :dnsbl_servers_list, :recaptcha_enabled, :recaptcha_site_key, :recaptcha_private_key, diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index c48175a4c5a..5efdd613e79 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -8,6 +8,11 @@ class RegistrationsController < Devise::RegistrationsController def create if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha + if Gitlab::IpCheck.new(request.remote_ip).spam? + flash[:alert] = 'Could not create an account. This IP is listed for spam.' + return render action: 'new' + end + super else flash[:alert] = "There was an error with the reCAPTCHA code below. Please re-enter the code." diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 6c6c2468374..db208884aa6 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -41,6 +41,8 @@ # recaptcha_site_key :string # recaptcha_private_key :string # metrics_port :integer default(8089) +# ip_blocking_enabled :boolean default(FALSE) +# dns_blacklist_threshold :float default(0.33) # class ApplicationSetting < ActiveRecord::Base diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 83f6814d822..4f915818d8f 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -212,6 +212,22 @@ %fieldset %legend Spam and Anti-bot Protection + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :ip_blocking_enabled do + = f.check_box :ip_blocking_enabled + Enable IP check against blacklist at sign-up + .help-block Helps preventing accounts creation from 'known spam sources' + + .form-group + = f.label :dnsbl_servers_list, class: 'control-label col-sm-2' do + DNSBL servers list + .col-sm-10 + = f.text_field :dnsbl_servers_list, class: 'form-control' + .help-block + Please enter DNSBL servers separated with comma + .form-group .col-sm-offset-2.col-sm-10 .checkbox diff --git a/db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb b/db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb new file mode 100644 index 00000000000..26606b10b54 --- /dev/null +++ b/db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb @@ -0,0 +1,6 @@ +class AddIpBlockingSettingsToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :ip_blocking_enabled, :boolean, default: false + add_column :application_settings, :dnsbl_servers_list, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index dc7cb9f667f..1457259ebaf 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160119145451) do +ActiveRecord::Schema.define(version: 20160120130905) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -62,6 +62,8 @@ ActiveRecord::Schema.define(version: 20160119145451) do t.string "recaptcha_private_key" t.integer "metrics_port", default: 8089 t.integer "metrics_sample_interval", default: 15 + t.boolean "ip_blocking_enabled", default: false + t.text "dnsbl_servers_list" end create_table "audit_events", force: :cascade do |t| diff --git a/lib/dnsxl_check.rb b/lib/dnsxl_check.rb new file mode 100644 index 00000000000..1e506b2d9cb --- /dev/null +++ b/lib/dnsxl_check.rb @@ -0,0 +1,105 @@ +require 'resolv' + +class DNSXLCheck + + class Resolver + def self.search(query) + begin + Resolv.getaddress(query) + true + rescue Resolv::ResolvError + false + end + end + end + + IP_REGEXP = /\A(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\z/ + DEFAULT_THRESHOLD = 0.33 + + def self.create_from_list(list) + dnsxl_check = DNSXLCheck.new + + list.each do |entry| + dnsxl_check.add_list(entry.domain, entry.weight) + end + + dnsxl_check + end + + def test(ip) + if use_threshold? + test_with_threshold(ip) + else + test_strict(ip) + end + end + + def test_with_threshold(ip) + return false if lists.empty? + + search(ip) + final_score >= threshold + end + + def test_strict(ip) + return false if lists.empty? + + search(ip) + @score > 0 + end + + def use_threshold=(value) + @use_threshold = value == true + end + + def use_threshold? + @use_threshold &&= true + end + + def threshold=(threshold) + raise ArgumentError, "'threshold' value must be grather than 0 and less than or equal to 1" unless threshold > 0 && threshold <= 1 + @threshold = threshold + end + + def threshold + @threshold ||= DEFAULT_THRESHOLD + end + + def add_list(domain, weight) + @lists ||= [] + @lists << { domain: domain, weight: weight } + end + + def lists + @lists ||= [] + end + + private + + def search(ip) + raise ArgumentError, "'ip' value must be in #{IP_REGEXP} format" unless ip.match(IP_REGEXP) + + @score = 0 + + reversed = reverse_ip(ip) + search_in_rbls(reversed) + end + + def reverse_ip(ip) + ip.split('.').reverse.join('.') + end + + def search_in_rbls(reversed_ip) + lists.each do |rbl| + query = "#{reversed_ip}.#{rbl[:domain]}" + @score += rbl[:weight] if Resolver.search(query) + end + end + + def final_score + weights = lists.map{ |rbl| rbl[:weight] }.reduce(:+).to_i + return 0 if weights == 0 + + (@score.to_f / weights.to_f).round(2) + end +end diff --git a/lib/gitlab/ip_check.rb b/lib/gitlab/ip_check.rb new file mode 100644 index 00000000000..f2e9b50d225 --- /dev/null +++ b/lib/gitlab/ip_check.rb @@ -0,0 +1,34 @@ +module Gitlab + class IpCheck + + def initialize(ip) + @ip = ip + + application_settings = ApplicationSetting.current + @ip_blocking_enabled = application_settings.ip_blocking_enabled + @dnsbl_servers_list = application_settings.dnsbl_servers_list + end + + def spam? + @ip_blocking_enabled && blacklisted? + end + + private + + def blacklisted? + on_dns_blacklist? + end + + def on_dns_blacklist? + dnsbl_check = DNSXLCheck.new + prepare_dnsbl_list(dnsbl_check) + dnsbl_check.test(@ip) + end + + def prepare_dnsbl_list(dnsbl_check) + @dnsbl_servers_list.split(',').map(&:strip).reject(&:empty?).each do |domain| + dnsbl_check.add_list(domain, 1) + end + end + end +end diff --git a/spec/lib/dnsxl_check_spec.rb b/spec/lib/dnsxl_check_spec.rb new file mode 100644 index 00000000000..a35a1be0c90 --- /dev/null +++ b/spec/lib/dnsxl_check_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' +require 'ostruct' + +describe 'DNSXLCheck', lib: true, no_db: true do + let(:spam_ip) { '127.0.0.2' } + let(:no_spam_ip) { '127.0.0.3' } + let(:invalid_ip) { 'a.b.c.d' } + let!(:dnsxl_check) { DNSXLCheck.create_from_list([OpenStruct.new({ domain: 'test', weight: 1 })]) } + + before(:context) do + class DNSXLCheck::Resolver + class << self + alias_method :old_search, :search + def search(query) + return false if query.match(/always\.failing\.domain\z/) + return true if query.match(/\A2\.0\.0\.127\./) + return false if query.match(/\A3\.0\.0\.127\./) + end + end + end + end + + describe '#test' do + before do + dnsxl_check.threshold = 0.75 + dnsxl_check.add_list('always.failing.domain', 1) + end + + context 'when threshold is used' do + before { dnsxl_check.use_threshold= true } + + it { expect(dnsxl_check.test(spam_ip)).to be_falsey } + end + + context 'when threshold is not used' do + before { dnsxl_check.use_threshold= false } + + it { expect(dnsxl_check.test(spam_ip)).to be_truthy } + end + end + + describe '#test_with_threshold' do + it { expect{ dnsxl_check.test_with_threshold(invalid_ip) }.to raise_error(ArgumentError) } + + it { expect(dnsxl_check.test_with_threshold(spam_ip)).to be_truthy } + it { expect(dnsxl_check.test_with_threshold(no_spam_ip)).to be_falsey } + end + + describe '#test_strict' do + before do + dnsxl_check.threshold = 1 + dnsxl_check.add_list('always.failing.domain', 1) + end + + it { expect{ dnsxl_check.test_strict(invalid_ip) }.to raise_error(ArgumentError) } + + it { expect(dnsxl_check.test_with_threshold(spam_ip)).to be_falsey } + it { expect(dnsxl_check.test_with_threshold(no_spam_ip)).to be_falsey } + it { expect(dnsxl_check.test_strict(spam_ip)).to be_truthy } + it { expect(dnsxl_check.test_strict(no_spam_ip)).to be_falsey } + end + + describe '#threshold=' do + it { expect{ dnsxl_check.threshold = 0 }.to raise_error(ArgumentError) } + it { expect{ dnsxl_check.threshold = 1.1 }.to raise_error(ArgumentError) } + it { expect{ dnsxl_check.threshold = 0.5 }.not_to raise_error } + end +end -- cgit v1.2.1 From 987e3d94b017cb94f55cf47cdbb417c45adf0681 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 20 Jan 2016 15:40:49 +0100 Subject: Fix MR diff_refs --- app/models/merge_request.rb | 4 ++-- app/views/projects/merge_requests/_new_submit.html.haml | 2 +- app/views/projects/merge_requests/show/_diffs.html.haml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 9511521879b..0a890bbf6db 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -518,7 +518,7 @@ class MergeRequest < ActiveRecord::Base @ci_commit ||= source_project.ci_commit(last_commit.id) if last_commit && source_project end - def diff_range - [last_commit.parent, first_commit] + def diff_refs + [first_commit.parent || first_commit, last_commit] end end diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 8b75976abd1..4c5a9818e3e 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -38,7 +38,7 @@ = render "projects/merge_requests/show/commits" #diffs.diffs.tab-pane.active - if @diffs.present? - = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @merge_request.diff_range + = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @merge_request.diff_refs - elsif @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE .alert.alert-danger %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits. diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index 46c6f79937b..64cd484193e 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -1,6 +1,6 @@ - if @merge_request_diff.collected? = render "projects/diffs/diffs", diffs: params[:w] == '1' ? @merge_request.diffs_no_whitespace : @merge_request.diffs, - project: @merge_request.project, diff_refs: @merge_request.diff_range + project: @merge_request.project, diff_refs: @merge_request.diff_refs - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} - else -- cgit v1.2.1 From a10ab94b068c31601c7d4ab0062b9d567af6cee2 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 13 Jan 2016 07:05:28 -0800 Subject: Gracefully handle invalid UTF-8 sequences in Markdown links Closes #6077 --- CHANGELOG | 1 + lib/banzai/filter/reference_filter.rb | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 2e0eee52a59..64121d05143 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,7 @@ v 8.4.0 (unreleased) - Autocomplete data is now always loaded, instead of when focusing a comment text area - Improved performance of finding issues for an entire group - Added custom application performance measuring system powered by InfluxDB + - Gracefully handle invalid UTF-8 sequences in Markdown links (Stan Hu) - Bump fog to 1.36.0 (Stan Hu) - Add user's last used IP addresses to admin page (Stan Hu) - Add housekeeping function to project settings page diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index 20bd4f7ee6e..3637b1bac94 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -133,6 +133,7 @@ module Banzai next unless link && text link = CGI.unescape(link) + next unless link.force_encoding('UTF-8').valid_encoding? # Ignore ending punctionation like periods or commas next unless link == text && text =~ /\A#{pattern}/ @@ -170,6 +171,7 @@ module Banzai next unless link && text link = CGI.unescape(link) + next unless link.force_encoding('UTF-8').valid_encoding? next unless link && link =~ /\A#{pattern}\z/ html = yield link, text -- cgit v1.2.1 From 0ad6b44a27baa260fb94569c354f30a54cf86d13 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 20 Jan 2016 16:15:19 +0100 Subject: Send user context to Sentry --- app/controllers/application_controller.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index bf99b2e777d..633c3f55614 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -15,6 +15,7 @@ class ApplicationController < ActionController::Base before_action :check_password_expiration before_action :check_2fa_requirement before_action :ldap_security_check + before_action :sentry_user_context before_action :default_headers before_action :add_gon_variables before_action :configure_permitted_parameters, if: :devise_controller? @@ -41,6 +42,16 @@ class ApplicationController < ActionController::Base protected + def sentry_user_context + if Rails.env.production? && current_application_settings.sentry_enabled && current_user + Raven.user_context( + id: current_user.id, + email: current_user.email, + username: current_user.username, + ) + end + end + # From https://github.com/plataformatec/devise/wiki/How-To:-Simple-Token-Authentication-Example # https://gist.github.com/josevalim/fb706b1e933ef01e4fb6 def authenticate_user_from_token! -- cgit v1.2.1 From 7838c957c23a36fb64125a1baf14454b735e9561 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 20 Jan 2016 16:18:42 +0100 Subject: Target '_blank' --- app/views/admin/application_settings/_form.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index f5a87933fde..35e4dd761ab 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -226,7 +226,7 @@ = f.text_field :recaptcha_site_key, class: 'form-control' .help-block Generate site and private keys here: - %a{ href: 'http://www.google.com/recaptcha', target: 'blank'} http://www.google.com/recaptcha + %a{ href: 'http://www.google.com/recaptcha', target: '_blank'} http://www.google.com/recaptcha .form-group = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2' .col-sm-10 @@ -244,7 +244,7 @@ Enable Sentry .help-block Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: - %a{ href: 'https://getsentry.com', target: 'blank' } https://getsentry.com + %a{ href: 'https://getsentry.com', target: '_blank' } https://getsentry.com .form-group = f.label :sentry_dsn, 'Sentry DSN', class: 'control-label col-sm-2' -- cgit v1.2.1 From 7377abf84a7fc20eb93938f73ce77ae26d3ec8ec Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Wed, 20 Jan 2016 10:50:26 -0500 Subject: Simplifies the HAML if statement for project branch URL --- app/views/layouts/header/_default.html.haml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index d1425cd0e88..fcb6b835a7e 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -37,10 +37,6 @@ %h1.title= title = render 'shared/outdated_browser' --if @project && !@project.empty_repo? && @ref +- if @project && !@project.empty_repo? :javascript - var findFileURL = "#{namespace_project_find_file_path(@project.namespace, @project, @ref)}"; - --elsif @project && !@project.empty_repo? - :javascript - var findFileURL = "#{namespace_project_find_file_path(@project.namespace, @project, @project.repository.root_ref)}"; \ No newline at end of file + var findFileURL = "#{namespace_project_find_file_path(@project.namespace, @project, @ref || @project.repository.root_ref)}"; \ No newline at end of file -- cgit v1.2.1 From 419a5e00eb83d3ae1fb9725200bb4a72cb8a52e2 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 20 Jan 2016 17:12:30 +0100 Subject: updated gitlab_git to fix issue #5858 and updated changelog --- CHANGELOG | 1 + Gemfile | 2 +- Gemfile.lock | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2e0eee52a59..03fce7e2157 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.5.0 (unreleased) - Add "visibility" flag to GET /projects api endpoint + - Upgrade gitlab_git to 7.2.23 to fix commit message mentions in first branch push v 8.4.0 (unreleased) - Allow LDAP users to change their email if it was not set by the LDAP server diff --git a/Gemfile b/Gemfile index 072f7a9fcc8..1c586adbfb5 100644 --- a/Gemfile +++ b/Gemfile @@ -49,7 +49,7 @@ gem "browser", '~> 1.0.0' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '~> 7.2.22' +gem "gitlab_git", '~> 7.2.23' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes diff --git a/Gemfile.lock b/Gemfile.lock index a14fdbeed23..acd7978c345 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -356,7 +356,7 @@ GEM posix-spawn (~> 0.3) gitlab_emoji (0.2.0) gemojione (~> 2.1) - gitlab_git (7.2.22) + gitlab_git (7.2.23) activesupport (~> 4.0) charlock_holmes (~> 0.7.3) github-linguist (~> 4.7.0) @@ -932,7 +932,7 @@ DEPENDENCIES github-markup (~> 1.3.1) gitlab-flowdock-git-hook (~> 1.0.1) gitlab_emoji (~> 0.2.0) - gitlab_git (~> 7.2.22) + gitlab_git (~> 7.2.23) gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.1.0) -- cgit v1.2.1 From 26f7d023e6d15f4deab39f74509bd6dddebf6974 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 20 Jan 2016 17:14:26 +0100 Subject: Update tests --- spec/fixtures/parallel_diff_result.yml | 2 +- spec/lib/gitlab/diff/highlight_spec.rb | 2 +- spec/lib/gitlab/diff/parallel_diff_spec.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/fixtures/parallel_diff_result.yml b/spec/fixtures/parallel_diff_result.yml index 0fb9bf6c5d8..a326b651aad 100644 --- a/spec/fixtures/parallel_diff_result.yml +++ b/spec/fixtures/parallel_diff_result.yml @@ -55,7 +55,7 @@ :type: new :number: 9 :text: | - + raise RuntimeError, "System commands must be given as an array of strings" + + raise RuntimeError, "System commands must be given as an array of strings" :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9 - :left: :type: diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index b54e95483d3..b84a57f357a 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -33,7 +33,7 @@ describe Gitlab::Diff::Highlight, lib: true do end it 'should highlight added lines' do - code = %Q{+ raise RuntimeError, "System commands must be given as an array of strings"\n} + code = %Q{+ raise RuntimeError, "System commands must be given as an array of strings"\n} expect(diff_lines[5].text).to eq(code) end diff --git a/spec/lib/gitlab/diff/parallel_diff_spec.rb b/spec/lib/gitlab/diff/parallel_diff_spec.rb index 852218b4e62..1c5bbc47120 100644 --- a/spec/lib/gitlab/diff/parallel_diff_spec.rb +++ b/spec/lib/gitlab/diff/parallel_diff_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::Diff::ParallelDiff, lib: true do include RepoHelpers - + let(:project) { create(:project) } let(:repository) { project.repository } let(:commit) { project.commit(sample_commit.id) } -- cgit v1.2.1 From 577f2fb47a7ef57415b78b49d5c746d4e99f6a98 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 20 Jan 2016 18:44:27 +0100 Subject: Save and use actual diff base commit for MR diff highlighting --- app/controllers/projects/compare_controller.rb | 4 +-- .../projects/merge_requests_controller.rb | 8 ++++-- app/helpers/diff_helper.rb | 3 +-- app/models/merge_request.rb | 15 ++++++++--- app/models/merge_request_diff.rb | 9 +++++++ ...3_add_base_commit_sha_to_merge_request_diffs.rb | 5 ++++ db/schema.rb | 29 +++++++++++----------- lib/gitlab/diff/file.rb | 12 +++++++-- lib/gitlab/diff/highlight.rb | 9 ++++++- lib/gitlab/diff/inline_diff_marker.rb | 13 +++++----- 10 files changed, 75 insertions(+), 32 deletions(-) create mode 100644 db/migrate/20160120172143_add_base_commit_sha_to_merge_request_diffs.rb diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 60360c8e5c8..f8ec76cd4e5 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -21,8 +21,8 @@ class Projects::CompareController < Projects::ApplicationController @commits = Commit.decorate(compare_result.commits, @project) @diffs = compare_result.diffs @commit = @project.commit(head_ref) - @first_commit = @project.commit(base_ref) - @diff_refs = [@first_commit, @commit] + @base_commit = @project.commit(base_ref) + @diff_refs = [@base_commit, @commit] @line_notes = [] end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index a6284a24223..ed3050d59aa 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -58,7 +58,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController def diffs @commit = @merge_request.last_commit - @first_commit = @merge_request.first_commit + @base_commit = @merge_request.diff_base_commit + + # MRs created before 8.4 don't have a diff_base_commit, + # but we need it for the "View file @ ..." link by deleted files + @base_commit ||= @merge_request.first_commit.parent || @merge_request.first_commit @comments_allowed = @reply_allowed = true @comments_target = { @@ -102,7 +106,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @source_project = merge_request.source_project @commits = @merge_request.compare_commits.reverse @commit = @merge_request.last_commit - @first_commit = @merge_request.first_commit + @base_commit = @merge_request.diff_base_commit @diffs = @merge_request.compare_diffs @ci_commit = @merge_request.ci_commit diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index f500d3572e0..62971d1e14b 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -102,8 +102,7 @@ module DiffHelper def commit_for_diff(diff) if diff.deleted_file - first_commit = @first_commit || @commit - first_commit.parent || @first_commit + @base_commit || @commit.parent || @commit else @commit end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 0a890bbf6db..41dd248d80a 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -180,6 +180,14 @@ class MergeRequest < ActiveRecord::Base merge_request_diff ? merge_request_diff.first_commit : compare_commits.first end + def diff_base_commit + if merge_request_diff + merge_request_diff.base_commit + else + self.target_project.commit(self.target_branch) + end + end + def last_commit_short_sha last_commit.short_id end @@ -477,8 +485,7 @@ class MergeRequest < ActiveRecord::Base end def target_sha - @target_sha ||= target_project. - repository.commit(target_branch).sha + @target_sha ||= target_project.repository.commit(target_branch).sha end def source_sha @@ -519,6 +526,8 @@ class MergeRequest < ActiveRecord::Base end def diff_refs - [first_commit.parent || first_commit, last_commit] + return nil unless diff_base_commit + + [diff_base_commit, last_commit] end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index c499a4b5b4c..ba0194cd0a6 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -73,6 +73,12 @@ class MergeRequestDiff < ActiveRecord::Base commits.last end + def base_commit + return nil unless self.base_commit_sha + + merge_request.target_project.commit(self.base_commit_sha) + end + def last_commit_short_sha @last_commit_short_sha ||= last_commit.short_id end @@ -156,6 +162,9 @@ class MergeRequestDiff < ActiveRecord::Base end self.st_diffs = new_diffs + + self.base_commit_sha = merge_request.target_project.commit(target_branch).try(:sha) + self.save end diff --git a/db/migrate/20160120172143_add_base_commit_sha_to_merge_request_diffs.rb b/db/migrate/20160120172143_add_base_commit_sha_to_merge_request_diffs.rb new file mode 100644 index 00000000000..d6c6aa4a4e8 --- /dev/null +++ b/db/migrate/20160120172143_add_base_commit_sha_to_merge_request_diffs.rb @@ -0,0 +1,5 @@ +class AddBaseCommitShaToMergeRequestDiffs < ActiveRecord::Migration + def change + add_column :merge_request_diffs, :base_commit_sha, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index dc7cb9f667f..7eb99a47c71 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160119145451) do +ActiveRecord::Schema.define(version: 20160120172143) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -490,6 +490,7 @@ ActiveRecord::Schema.define(version: 20160119145451) do t.integer "merge_request_id", null: false t.datetime "created_at" t.datetime "updated_at" + t.string "base_commit_sha" end add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree @@ -725,19 +726,19 @@ ActiveRecord::Schema.define(version: 20160119145451) do t.string "type" t.string "title" t.integer "project_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "active", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "active", null: false t.text "properties" - t.boolean "template", default: false - t.boolean "push_events", default: true - t.boolean "issues_events", default: true - t.boolean "merge_requests_events", default: true - t.boolean "tag_push_events", default: true - t.boolean "note_events", default: true, null: false - t.boolean "build_events", default: false, null: false - t.string "category", default: "common", null: false - t.boolean "default", default: false + t.boolean "template", default: false + t.boolean "push_events", default: true + t.boolean "issues_events", default: true + t.boolean "merge_requests_events", default: true + t.boolean "tag_push_events", default: true + t.boolean "note_events", default: true, null: false + t.boolean "build_events", default: false, null: false + t.string "category", default: "common", null: false + t.boolean "default", default: false end add_index "services", ["category"], name: "index_services_on_category", using: :btree @@ -854,7 +855,7 @@ ActiveRecord::Schema.define(version: 20160119145451) do t.boolean "hide_project_limit", default: false t.string "unlock_token" t.datetime "otp_grace_period_started_at" - t.boolean "ldap_email", default: false, null: false + t.boolean "ldap_email", default: false, null: false end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index 74b1c117129..a484177ae33 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -1,14 +1,22 @@ module Gitlab module Diff class File - attr_reader :diff, :new_ref, :old_ref + attr_reader :diff, :diff_refs delegate :new_file, :deleted_file, :renamed_file, :old_path, :new_path, to: :diff, prefix: false def initialize(diff, diff_refs) @diff = diff - @old_ref, @new_ref = diff_refs + @diff_refs = diff_refs + end + + def old_ref + diff_refs[0] if diff_refs + end + + def new_ref + diff_refs[1] if diff_refs end # Array of Gitlab::DIff::Line objects diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index b6875f07279..fe084a7399a 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -31,6 +31,8 @@ module Gitlab private def highlight_line(diff_line, index) + return html_escape(diff_line.text) unless diff_file.diff_refs + line_prefix = diff_line.text.match(/\A([+-])/) ? $1 : ' ' case diff_line.type @@ -42,7 +44,7 @@ module Gitlab # Only update text if line is found. This will prevent # issues with submodules given the line only exists in diff content. - rich_line ? line_prefix + rich_line : diff_line.text + rich_line ? line_prefix + rich_line : html_escape(diff_line.text) end def inline_diffs @@ -63,6 +65,11 @@ module Gitlab [ref.project.repository, ref.id, path] end + + def html_escape(str) + replacements = { '&' => '&', '>' => '>', '<' => '<', '"' => '"', "'" => ''' } + str.gsub(/[&"'><]/, replacements) + end end end end diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb index c31149d374e..7ca198eeb86 100644 --- a/lib/gitlab/diff/inline_diff_marker.rb +++ b/lib/gitlab/diff/inline_diff_marker.rb @@ -9,17 +9,18 @@ module Gitlab end def mark(line_inline_diffs) - offset = 0 + marker_ranges = [] line_inline_diffs.each do |inline_diff_range| # Map the inline-diff range based on the raw line to character positions in the rich line inline_diff_positions = position_mapping[inline_diff_range].flatten # Turn the array of character positions into ranges - marker_ranges = collapse_ranges(inline_diff_positions) + marker_ranges.concat(collapse_ranges(inline_diff_positions)) + end - # Mark each range - marker_ranges.each do |range| - offset = insert_around_range(rich_line, range, "", "", offset) - end + offset = 0 + # Mark each range + marker_ranges.each do |range| + offset = insert_around_range(rich_line, range, "", "", offset) end rich_line -- cgit v1.2.1 From 0e992a3b4e710f8486a37bfa73ad6981365fceb2 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 20 Jan 2016 19:20:13 +0100 Subject: Properly highlight lines around '\ No newline at end of file' --- app/views/projects/diffs/_parallel_view.html.haml | 5 +++++ app/views/projects/diffs/_text_file.html.haml | 4 ++++ lib/gitlab/diff/highlight.rb | 4 ++-- lib/gitlab/diff/parallel_diff.rb | 2 +- lib/gitlab/diff/parser.rb | 16 ++++++++++++---- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 986d30d36a8..a15d147ab05 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -8,6 +8,11 @@ - if left[:type] == 'match' = render "projects/diffs/match_line_parallel", { line: left[:text], line_old: left[:number], line_new: right[:number] } + - elsif left[:type] == 'nonewline' + %td.old_line + %td.line_content.parallel.matched= left[:text] + %td.new_line + %td.line_content.parallel.matched= left[:text] - else %td.old_line{id: left[:line_code], class: "#{left[:type]}"} = link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code] diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index 310bbd4efea..f4fc6caba0f 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -15,6 +15,10 @@ - if type == "match" = render "projects/diffs/match_line", {line: line.text, line_old: line_old, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file} + - elsif type == 'nonewline' + %td.old_line.diff-line-num + %td.new_line.diff-line-num + %td.line_content.matched= line.text - else %td.old_line = link_to raw(type == "new" ? " " : line_old), "##{line_code}", id: line_code diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index fe084a7399a..179f8164c84 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -14,7 +14,7 @@ module Gitlab def highlight @diff_lines.each_with_index do |diff_line, i| # ignore highlighting for "match" lines - next if diff_line.type == 'match' + next if diff_line.type == 'match' || diff_line.type == 'nonewline' rich_line = highlight_line(diff_line, i) @@ -33,7 +33,7 @@ module Gitlab def highlight_line(diff_line, index) return html_escape(diff_line.text) unless diff_file.diff_refs - line_prefix = diff_line.text.match(/\A([+-])/) ? $1 : ' ' + line_prefix = diff_line.text.match(/\A(.)/) ? $1 : ' ' case diff_line.type when 'new', nil diff --git a/lib/gitlab/diff/parallel_diff.rb b/lib/gitlab/diff/parallel_diff.rb index a0dc3da875d..c0db3559e3a 100644 --- a/lib/gitlab/diff/parallel_diff.rb +++ b/lib/gitlab/diff/parallel_diff.rb @@ -62,7 +62,7 @@ module Gitlab } } skip_next = true - when 'old', nil + when 'old', 'nonewline', nil # Left side has text removed, right side doesn't have any change # No next line code, no new line number, no new line text lines << { diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb index 6c8a1fc6d6f..3666063bf8b 100644 --- a/lib/gitlab/diff/parser.rb +++ b/lib/gitlab/diff/parser.rb @@ -26,6 +26,10 @@ module Gitlab lines_obj << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new) line_obj_index += 1 next + elsif line[0] == '\\' + type = 'nonewline' + lines_obj << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new) + line_obj_index += 1 else type = identification_type(line) lines_obj << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new) @@ -33,10 +37,13 @@ module Gitlab end - if line[0] == "+" + case line[0] + when "+" line_new += 1 - elsif line[0] == "-" + when "-" line_old += 1 + when "\\" + # No increment else line_new += 1 line_old += 1 @@ -59,9 +66,10 @@ module Gitlab end def identification_type(line) - if line[0] == "+" + case line[0] + when "+" "new" - elsif line[0] == "-" + when "-" "old" else nil -- cgit v1.2.1 From 34b132d49ae2552104e937642495203598ad59cf Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Wed, 20 Jan 2016 13:24:52 -0500 Subject: Fix term height so text overflap does not happen. --- app/assets/stylesheets/pages/search.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index bdcf1897522..3aaa96da609 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -3,10 +3,6 @@ border-bottom: 1px solid #DDD; padding-bottom: 15px; margin-bottom: 15px; - - .term { - height: 22px; - } } } -- cgit v1.2.1 From 6000f8545f43b449035cb50382901ce40fb807b0 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 20 Jan 2016 19:33:34 +0100 Subject: Validate bounds just to be sure --- lib/gitlab/diff/inline_diff_marker.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb index 7ca198eeb86..1d7fa1bce06 100644 --- a/lib/gitlab/diff/inline_diff_marker.rb +++ b/lib/gitlab/diff/inline_diff_marker.rb @@ -93,6 +93,9 @@ module Gitlab # Inserts tags around the characters identified by the given range def insert_around_range(text, range, before, after, offset = 0) + # Just to be sure + return offset if offset + range.end + 1 > text.length + text.insert(offset + range.begin, before) offset += before.length -- cgit v1.2.1 From 7a0bde935ae487c172e80cbd0c965db86f640634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20HULARD?= Date: Wed, 20 Jan 2016 18:38:15 +0000 Subject: Fix a missing letter in the documentation. --- doc/ci/languages/php.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/languages/php.md b/doc/ci/languages/php.md index dacb67fa3ff..77f9fae5bb6 100644 --- a/doc/ci/languages/php.md +++ b/doc/ci/languages/php.md @@ -12,7 +12,7 @@ configuration from the developer. To overcome this we will be using the official [PHP docker image][php-hub] that can be found in Docker Hub. This will allow us to test PHP projects against different versions of PHP. -However, not everything is plug 'n' play, you still need to onfigure some +However, not everything is plug 'n' play, you still need to configure some things manually. As with every build, you need to create a valid `.gitlab-ci.yml` describing the -- cgit v1.2.1 From 86b75f4713fa6cb14cbf443c1e29ca6384204b65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 20 Jan 2016 14:38:34 -0500 Subject: Use a MR with commits. --- spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb index 449cecaa789..de9fed2b7dd 100644 --- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb @@ -6,7 +6,7 @@ describe MergeRequests::MergeWhenBuildSucceedsService do let(:mr_merge_if_green_enabled) do create(:merge_request, merge_when_build_succeeds: true, merge_user: user, - source_branch: "source_branch", target_branch: project.default_branch, + source_branch: "master", target_branch: 'feature', source_project: project, target_project: project, state: "opened") end -- cgit v1.2.1 From 9b0f57781e8c71eb0e627a63078fedcedfe62bbb Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 20 Jan 2016 21:48:22 +0100 Subject: Add method that calculates total size for artifacts subfolder --- app/models/ci/build.rb | 8 ++++---- app/views/projects/builds/show.html.haml | 2 +- lib/gitlab/ci/build/artifacts/metadata.rb | 8 +++++--- lib/gitlab/ci/build/artifacts/metadata/entry.rb | 7 +++++++ .../gitlab/ci/build/artifacts/metadata/entry_spec.rb | 15 ++++++++++----- spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb | 17 +++++++++++++++-- spec/models/build_spec.rb | 8 ++++---- 7 files changed, 46 insertions(+), 19 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 16a5b03f591..623edd8bc57 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -346,17 +346,17 @@ module Ci end def artifacts_browse_url - if artifacts_browser_supported? + if artifacts_metadata? browse_namespace_project_build_artifacts_path(project.namespace, project, self) end end - def artifacts_browser_supported? + def artifacts_metadata? artifacts? && artifacts_metadata.exists? end - def artifacts_metadata_entry(path) - Gitlab::Ci::Build::Artifacts::Metadata.new(artifacts_metadata.path, path).to_entry + def artifacts_metadata_entry(path, **options) + Gitlab::Ci::Build::Artifacts::Metadata.new(artifacts_metadata.path, path, **options).to_entry end private diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 2be572d3b10..ba1fdc6f0e7 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -96,7 +96,7 @@ .center .btn-group{ role: :group } = link_to "Download", @build.artifacts_download_url, class: 'btn btn-sm btn-primary' - - if @build.artifacts_browser_supported? + - if @build.artifacts_metadata? = link_to "Browse", @build.artifacts_browse_url, class: 'btn btn-sm btn-primary' .build-widget diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb index 1344f5d120b..f2020c82d40 100644 --- a/lib/gitlab/ci/build/artifacts/metadata.rb +++ b/lib/gitlab/ci/build/artifacts/metadata.rb @@ -13,8 +13,8 @@ module Gitlab attr_reader :file, :path, :full_version - def initialize(file, path) - @file, @path = file, path + def initialize(file, path, **opts) + @file, @path, @opts = file, path, opts @full_version = read_version end @@ -52,7 +52,9 @@ module Gitlab def match_entries(gz) entries = {} - match_pattern = %r{^#{Regexp.escape(@path)}[^/]*/?$} + + child_pattern = '[^/]*/?$' unless @opts[:recursive] + match_pattern = /^#{Regexp.escape(@path)}#{child_pattern}/ until gz.eof? do begin diff --git a/lib/gitlab/ci/build/artifacts/metadata/entry.rb b/lib/gitlab/ci/build/artifacts/metadata/entry.rb index 25b71fc3275..7f4c750b6fd 100644 --- a/lib/gitlab/ci/build/artifacts/metadata/entry.rb +++ b/lib/gitlab/ci/build/artifacts/metadata/entry.rb @@ -95,6 +95,13 @@ module Gitlab children.empty? end + def total_size + descendant_pattern = %r{^#{Regexp.escape(@path)}} + entries.sum do |path, entry| + (entry[:size] if path =~ descendant_pattern).to_i + end + end + def to_s @path end diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb index 41257103ead..acca0b08bab 100644 --- a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb +++ b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb @@ -4,13 +4,13 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do let(:entries) do { 'path/' => {}, 'path/dir_1/' => {}, - 'path/dir_1/file_1' => {}, - 'path/dir_1/file_b' => {}, + 'path/dir_1/file_1' => { size: 10 }, + 'path/dir_1/file_b' => { size: 10 }, 'path/dir_1/subdir/' => {}, - 'path/dir_1/subdir/subfile' => {}, + 'path/dir_1/subdir/subfile' => { size: 10 }, 'path/second_dir' => {}, - 'path/second_dir/dir_3/file_2' => {}, - 'path/second_dir/dir_3/file_3'=> {}, + 'path/second_dir/dir_3/file_2' => { size: 10 }, + 'path/second_dir/dir_3/file_3'=> { size: 10 }, 'another_directory/'=> {}, 'another_file' => {}, '/file/with/absolute_path' => {} } @@ -112,6 +112,11 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do subject { |example| path(example).empty? } it { is_expected.to be false } end + + describe '#total_size' do + subject { |example| path(example).total_size } + it { is_expected.to eq(30) } + end end end diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb index 828eedfa7b0..eea01f91879 100644 --- a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb +++ b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' describe Gitlab::Ci::Build::Artifacts::Metadata do - def metadata(path = '') - described_class.new(metadata_file_path, path) + def metadata(path = '', **opts) + described_class.new(metadata_file_path, path, **opts) end let(:metadata_file_path) do @@ -51,6 +51,19 @@ describe Gitlab::Ci::Build::Artifacts::Metadata do end end + describe '#find_entries! recursively for other_artifacts_0.1.2/' do + subject { metadata('other_artifacts_0.1.2/', recursive: true).find_entries! } + + it 'matches correct paths' do + expect(subject.keys). + to contain_exactly 'other_artifacts_0.1.2/', + 'other_artifacts_0.1.2/doc_sample.txt', + 'other_artifacts_0.1.2/another-subdirectory/', + 'other_artifacts_0.1.2/another-subdirectory/empty_directory/', + 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' + end + end + describe '#to_entry' do subject { metadata('').to_entry } it { is_expected.to be_an_instance_of(Gitlab::Ci::Build::Artifacts::Metadata::Entry) } diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index d12b9e65c82..d30bc7d0554 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -362,12 +362,12 @@ describe Ci::Build, models: true do subject { build.artifacts_browse_url } it "should be nil if artifacts browser is unsupported" do - allow(build).to receive(:artifacts_browser_supported?).and_return(false) + allow(build).to receive(:artifacts_metadata?).and_return(false) is_expected.to be_nil end it 'should not be nil if artifacts browser is supported' do - allow(build).to receive(:artifacts_browser_supported?).and_return(true) + allow(build).to receive(:artifacts_metadata?).and_return(true) is_expected.to_not be_nil end end @@ -391,8 +391,8 @@ describe Ci::Build, models: true do end - describe :artifacts_browser_supported? do - subject { build.artifacts_browser_supported? } + describe :artifacts_metadata? do + subject { build.artifacts_metadata? } context 'artifacts metadata does not exist' do it { is_expected.to be_falsy } end -- cgit v1.2.1 From cd7f27543b4d676424785a6c3d16850909341dba Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 20 Jan 2016 22:32:10 +0100 Subject: Mention required Ruby version [ci skip] --- doc/install/installation.md | 15 +++++++++++++-- doc/install/requirements.md | 11 +++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index 00030729a4b..4772ed3c566 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -107,9 +107,16 @@ Then select 'Internet Site' and press enter to confirm the hostname. ## 2. Ruby -The use of Ruby version managers such as [RVM](https://rvm.io/), [rbenv](https://github.com/sstephenson/rbenv) or [chruby](https://github.com/postmodern/chruby) with GitLab in production frequently leads to hard to diagnose problems. For example, GitLab Shell is called from OpenSSH and having a version manager can prevent pushing and pulling over SSH. Version managers are not supported and we strongly advise everyone to follow the instructions below to use a system Ruby. +_**Note:** The current supported Ruby versions are 2.1.x. Ruby 2.2 and 2.3 are +currently not supported._ -Remove the old Ruby 1.8 if present +The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab +in production, frequently leads to hard to diagnose problems. For example, +GitLab Shell is called from OpenSSH, and having a version manager can prevent +pushing and pulling over SSH. Version managers are not supported and we strongly +advise everyone to follow the instructions below to use a system Ruby. + +Remove the old Ruby 1.8 if present: sudo apt-get remove ruby1.8 @@ -555,3 +562,7 @@ this is likely due to an outdated Nginx or Apache configuration, or a missing or misconfigured gitlab-workhorse instance. Double-check that you've [installed Go](#3-go), [installed gitlab-workhorse](#install-gitlab-workhorse), and correctly [configured Nginx](#site-configuration). + +[RVM]: https://rvm.io/ "RVM Homepage" +[rbenv]: https://github.com/sstephenson/rbenv "rbenv on GitHub" +[chruby]: https://github.com/postmodern/chruby "chruby on GitHub" diff --git a/doc/install/requirements.md b/doc/install/requirements.md index c0ccdd37458..c0425f27ab1 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -32,15 +32,18 @@ Please consider using a virtual machine to run GitLab. ## Ruby versions -GitLab requires Ruby (MRI) 2.1 +GitLab requires Ruby (MRI) 2.1.x and currently does not work with versions 2.2 +and 2.3. + You will have to use the standard MRI implementation of Ruby. -We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab needs several Gems that have native extensions. +We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab +needs several Gems that have native extensions. ## Hardware requirements ### Storage -The necessary hard drive space largely depends on the size of the repos you want to store in GitLab but as a *rule of thumb* you should have at least as much free space as all your repos combined take up. +The necessary hard drive space largely depends on the size of the repos you want to store in GitLab but as a *rule of thumb* you should have at least as much free space as all your repos combined take up. If you want to be flexible about growing your hard drive space in the future consider mounting it using LVM so you can add more hard drives when you need them. @@ -109,4 +112,4 @@ On a very active server (10,000 active users) the Sidekiq process can use 1GB+ o - Firefox (Latest released version and [latest ESR version](https://www.mozilla.org/en-US/firefox/organizations/)) - Safari 7+ (known problem: required fields in html5 do not work) - Opera (Latest released version) -- Internet Explorer (IE) 10+ but please make sure that you have the `Compatibility View` mode disabled. \ No newline at end of file +- Internet Explorer (IE) 10+ but please make sure that you have the `Compatibility View` mode disabled. -- cgit v1.2.1 From f4bdbecfd54c39baed79d0143de97933d26e3215 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Wed, 20 Jan 2016 19:36:35 -0200 Subject: Fix cross projects detection when importing GitHub pull requests --- lib/gitlab/github_import/pull_request_formatter.rb | 6 ++++- .../github_import/pull_request_formatter_spec.rb | 27 ++++++++++++++-------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index b7c47958cc7..f96fed0f5cf 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -18,7 +18,7 @@ module Gitlab end def cross_project? - source_repo.fork == true + source_repo.id != target_repo.id end def number @@ -73,6 +73,10 @@ module Gitlab project end + def target_repo + raw_data.base.repo + end + def target_branch target_project.repository.find_branch(raw_data.base.ref) end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index 9aefec77f6d..6cebcb5009a 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -2,8 +2,11 @@ require 'spec_helper' describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:project) { create(:project) } - let(:source_branch) { OpenStruct.new(ref: 'feature') } - let(:target_branch) { OpenStruct.new(ref: 'master') } + let(:repository) { OpenStruct.new(id: 1, fork: false) } + let(:source_repo) { repository } + let(:source_branch) { OpenStruct.new(ref: 'feature', repo: source_repo) } + let(:target_repo) { repository } + let(:target_branch) { OpenStruct.new(ref: 'master', repo: target_repo) } let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } @@ -125,10 +128,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end describe '#cross_project?' do - context 'when source repo is not a fork' do - let(:local_repo) { OpenStruct.new(fork: false) } - let(:source_branch) { OpenStruct.new(ref: 'feature', repo: local_repo) } - let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch)) } + context 'when source, and target repositories are the same' do + let(:raw_data) { OpenStruct.new(base_data) } it 'returns false' do expect(pull_request.cross_project?).to eq false @@ -136,9 +137,17 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end context 'when source repo is a fork' do - let(:forked_repo) { OpenStruct.new(fork: true) } - let(:source_branch) { OpenStruct.new(ref: 'feature', repo: forked_repo) } - let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch)) } + let(:source_repo) { OpenStruct.new(id: 2, fork: true) } + let(:raw_data) { OpenStruct.new(base_data) } + + it 'returns true' do + expect(pull_request.cross_project?).to eq true + end + end + + context 'when target repo is a fork' do + let(:target_repo) { OpenStruct.new(id: 2, fork: true) } + let(:raw_data) { OpenStruct.new(base_data) } it 'returns true' do expect(pull_request.cross_project?).to eq true -- cgit v1.2.1 From 12da27090f0a8ff4b05023a5216738413dcbe110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 20 Jan 2016 22:39:32 +0100 Subject: Address some JS coding-style and HTML semantic issues - Replace 'rel' attributes with 'data' or 'id' attributes - Style jQuery methods chaining according to Jacob's guidelines - Rename a method --- app/assets/javascripts/notes.js.coffee | 53 ++++++++++++---------- .../notes/_diff_notes_with_reply.html.haml | 2 +- .../_diff_notes_with_reply_parallel.html.haml | 5 +- app/views/projects/notes/_note.html.haml | 2 +- .../projects/notes/discussions/_commit.html.haml | 3 +- 5 files changed, 33 insertions(+), 32 deletions(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 3927868cdd4..fd6f89be98d 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -130,11 +130,11 @@ class @Notes else if @isNewNote(note) @note_ids.push(note.id) - $('ul.main-notes-list'). - append(note.html). - syntaxHighlight() + $('ul.main-notes-list') + .append(note.html) + .syntaxHighlight() @initTaskList() - @incrementNotesCount() + @updateNotesCount(1) ### @@ -155,13 +155,13 @@ class @Notes return unless @isNewNote(note) @note_ids.push(note.id) - form = $("form[rel='" + note.discussion_id + "']") + form = $("#new-discussion-note-form-#{note.discussion_id}") row = form.closest("tr") note_html = $(note.html) note_html.syntaxHighlight() # is this the first note of discussion? - discussionContainer = $(".notes[rel='" + note.discussion_id + "']") + discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']") if discussionContainer.length is 0 # insert the note and the reply button after the temp row row.after note.discussion_html @@ -170,21 +170,21 @@ class @Notes row.next().find(".note").remove() # Before that, the container didn't exist - discussionContainer = $(".notes[rel='" + note.discussion_id + "']") + discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']") # Add note to 'Changes' page discussions discussionContainer.append note_html # Init discussion on 'Discussion' page if it is merge request page if $('body').attr('data-page').indexOf('projects:merge_request') is 0 - $('ul.main-notes-list'). - append(note.discussion_with_diff_html). - syntaxHighlight() + $('ul.main-notes-list') + .append(note.discussion_with_diff_html) + .syntaxHighlight() else # append new note to all matching discussions discussionContainer.append note_html - @incrementNotesCount() + @updateNotesCount(1) ### Called in response the main target form has been successfully submitted. @@ -292,7 +292,7 @@ class @Notes @renderDiscussionNote(note) # cleanup after successfully creating a diff/discussion note - @removeDiscussionNoteForm($("form[rel='" + note.discussion_id + "']")) + @removeDiscussionNoteForm($("#new-discussion-note-form-#{note.discussion_id}")) ### Called in response to the edit note form being submitted @@ -366,24 +366,30 @@ class @Notes Removes the whole discussion if the last note is being removed. ### removeNote: (e) => - noteId = $(e.currentTarget).closest(".note").attr("id") - - $('.note[id="' + noteId + '"]').each (i, el) => - note = $(el) + noteId = $(e.currentTarget) + .closest(".note") + .attr("id") + + # A same note appears in the "Discussion" and in the "Changes" tab, we have + # to remove all. Using $(".note[id='noteId']") ensure we get all the notes, + # where $("#noteId") would return only one. + $(".note[id='#{noteId}']").each (i, el) => + note = $(el) notes = note.closest(".notes") # check if this is the last note for this line if notes.find(".note").length is 1 - # for discussions - notes.closest(".discussion").remove() + # "Discussions" tab + notes.closest(".timeline-entry").remove() - # for diff lines + # "Changes" tab / commit view notes.closest("tr").remove() note.remove() - @decrementNotesCount() + # Decrement the "Discussions" counter only once + @updateNotesCount(-1) ### Called in response to clicking the delete attachment link @@ -424,7 +430,7 @@ class @Notes ### setupDiscussionNoteForm: (dataHolder, form) => # setup note target - form.attr "rel", dataHolder.data("discussionId") + form.attr 'id', "new-discussion-note-form-#{dataHolder.data("discussionId")}" form.find("#line_type").val dataHolder.data("lineType") form.find("#note_commit_id").val dataHolder.data("commitId") form.find("#note_line_code").val dataHolder.data("lineCode") @@ -555,8 +561,5 @@ class @Notes updateTaskList: -> $('form', this).submit() - incrementNotesCount: (incrementStep = 1) -> + updateNotesCount: (incrementStep) -> @notesCountBadge.text parseInt(@notesCountBadge.text()) + incrementStep - - decrementNotesCount: -> - @incrementNotesCount(-1) diff --git a/app/views/projects/notes/_diff_notes_with_reply.html.haml b/app/views/projects/notes/_diff_notes_with_reply.html.haml index c731baf0a65..11f9859a90f 100644 --- a/app/views/projects/notes/_diff_notes_with_reply.html.haml +++ b/app/views/projects/notes/_diff_notes_with_reply.html.haml @@ -7,7 +7,7 @@ %i.fa.fa-comment = notes.count %td.notes_content - %ul.notes{ rel: note.discussion_id } + %ul.notes{ data: { discussion_id: note.discussion_id } } = render notes .discussion-reply-holder = link_to_reply_diff(note) diff --git a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml index c6726cbafa3..bb761ed2f94 100644 --- a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml +++ b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml @@ -8,7 +8,7 @@ %i.fa.fa-comment = notes_left.count %td.notes_content.parallel.old - %ul.notes{ rel: note1.discussion_id } + %ul.notes{ data: { discussion_id: note1.discussion_id } } = render notes_left .discussion-reply-holder @@ -23,7 +23,7 @@ %i.fa.fa-comment = notes_right.count %td.notes_content.parallel.new - %ul.notes{ rel: note2.discussion_id } + %ul.notes{ data: { discussion_id: note2.discussion_id } } = render notes_right .discussion-reply-holder @@ -31,4 +31,3 @@ - else %td.notes_line.new= "" %td.notes_content.parallel.new= "" - diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 922535e5c4a..e858c412836 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -1,4 +1,4 @@ -%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)], data: { discussion: note.discussion_id } } +%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)] } .timeline-entry-inner .timeline-icon %a{href: user_path(note.author)} diff --git a/app/views/projects/notes/discussions/_commit.html.haml b/app/views/projects/notes/discussions/_commit.html.haml index 6903fad4a0a..3da2f2060b8 100644 --- a/app/views/projects/notes/discussions/_commit.html.haml +++ b/app/views/projects/notes/discussions/_commit.html.haml @@ -20,8 +20,7 @@ = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note - else .panel.panel-default - .notes{ rel: discussion_notes.first.discussion_id } + .notes{ data: { discussion_id: discussion_notes.first.discussion_id } } = render discussion_notes .discussion-reply-holder = link_to_reply_diff(discussion_notes.first) - -- cgit v1.2.1 From 82bc7679e782059568402a282905d3d607dbb97d Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 20 Jan 2016 23:02:24 +0100 Subject: Move integration/metrics to monitoring/performance [ci skip] --- doc/README.md | 2 +- doc/integration/metrics/gitlab_configuration.md | 39 ----- .../img/metrics_gitlab_configuration_settings.png | Bin 45148 -> 0 bytes doc/integration/metrics/influxdb_configuration.md | 192 --------------------- doc/integration/metrics/influxdb_schema.md | 87 ---------- doc/integration/metrics/introduction.md | 64 ------- doc/monitoring/performance/gitlab_configuration.md | 39 +++++ .../img/metrics_gitlab_configuration_settings.png | Bin 0 -> 45148 bytes .../performance/influxdb_configuration.md | 192 +++++++++++++++++++++ doc/monitoring/performance/influxdb_schema.md | 87 ++++++++++ doc/monitoring/performance/introduction.md | 64 +++++++ 11 files changed, 383 insertions(+), 383 deletions(-) delete mode 100644 doc/integration/metrics/gitlab_configuration.md delete mode 100644 doc/integration/metrics/img/metrics_gitlab_configuration_settings.png delete mode 100644 doc/integration/metrics/influxdb_configuration.md delete mode 100644 doc/integration/metrics/influxdb_schema.md delete mode 100644 doc/integration/metrics/introduction.md create mode 100644 doc/monitoring/performance/gitlab_configuration.md create mode 100644 doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png create mode 100644 doc/monitoring/performance/influxdb_configuration.md create mode 100644 doc/monitoring/performance/influxdb_schema.md create mode 100644 doc/monitoring/performance/introduction.md diff --git a/doc/README.md b/doc/README.md index 05c0d6fa0fe..1efed2871f9 100644 --- a/doc/README.md +++ b/doc/README.md @@ -67,7 +67,7 @@ - [Reply by email](incoming_email/README.md) Allow users to comment on issues and merge requests by replying to notification emails. - [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE. - [Git LFS configuration](workflow/lfs/lfs_administration.md) -- [GitLab Performance Monitoring](integration/metrics/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics +- [GitLab Performance Monitoring](monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics ## Contributor documentation diff --git a/doc/integration/metrics/gitlab_configuration.md b/doc/integration/metrics/gitlab_configuration.md deleted file mode 100644 index b856e7935a3..00000000000 --- a/doc/integration/metrics/gitlab_configuration.md +++ /dev/null @@ -1,39 +0,0 @@ -# GitLab Configuration - -GitLab Performance Monitoring is disabled by default. To enable it and change any of its -settings, navigate to the Admin area in **Settings > Metrics** -(`/admin/application_settings`). - -The minimum required settings you need to set are the InfluxDB host and port. -Make sure _Enable InfluxDB Metrics_ is checked and hit **Save** to save the -changes. - ---- - -![GitLab Performance Monitoring Admin Settings](img/metrics_gitlab_configuration_settings.png) - ---- - -Finally, a restart of all GitLab processes is required for the changes to take -effect: - -```bash -# For Omnibus installations -sudo gitlab-ctl restart - -# For installations from source -sudo service gitlab restart -``` - -## Pending Migrations - -When any migrations are pending, the metrics are disabled until the migrations -have been performed. - ---- - -Read more on: - -- [Introduction to GitLab Performance Monitoring](introduction.md) -- [InfluxDB Configuration](influxdb_configuration.md) -- [InfluxDB Schema](influxdb_schema.md) diff --git a/doc/integration/metrics/img/metrics_gitlab_configuration_settings.png b/doc/integration/metrics/img/metrics_gitlab_configuration_settings.png deleted file mode 100644 index 14d82b6ac98..00000000000 Binary files a/doc/integration/metrics/img/metrics_gitlab_configuration_settings.png and /dev/null differ diff --git a/doc/integration/metrics/influxdb_configuration.md b/doc/integration/metrics/influxdb_configuration.md deleted file mode 100644 index df5e4af7fad..00000000000 --- a/doc/integration/metrics/influxdb_configuration.md +++ /dev/null @@ -1,192 +0,0 @@ -# InfluxDB Configuration - -The default settings provided by [InfluxDB] are not sufficient for a high traffic -GitLab environment. The settings discussed in this document are based on the -settings GitLab uses for GitLab.com, depending on your own needs you may need to -further adjust them. - -If you are intending to run InfluxDB on the same server as GitLab, make sure -you have plenty of RAM since InfluxDB can use quite a bit depending on traffic. - -Unless you are going with a budget setup, it's advised to run it separately. - -## Requirements - -- InfluxDB 0.9.5 or newer -- A fairly modern version of Linux -- At least 4GB of RAM -- At least 10GB of storage for InfluxDB data - -Note that the RAM and storage requirements can differ greatly depending on the -amount of data received/stored. To limit the amount of stored data users can -look into [InfluxDB Retention Policies][influxdb-retention]. - -## Installation - -Installing InfluxDB is out of the scope of this document. Please refer to the -[InfluxDB documentation]. - -## InfluxDB Server Settings - -Since InfluxDB has many settings that users may wish to customize themselves -(e.g. what port to run InfluxDB on), we'll only cover the essentials. - -The configuration file in question is usually located at -`/etc/influxdb/influxdb.conf`. Whenever you make a change in this file, -InfluxDB needs to be restarted. - -### Storage Engine - -InfluxDB comes with different storage engines and as of InfluxDB 0.9.5 a new -storage engine is available, called [TSM Tree]. All users **must** use the new -`tsm1` storage engine as this [will be the default engine][tsm1-commit] in -upcoming InfluxDB releases. - -Make sure you have the following in your configuration file: - -``` -[data] - dir = "/var/lib/influxdb/data" - engine = "tsm1" -``` - -### Admin Panel - -Production environments should have the InfluxDB admin panel **disabled**. This -feature can be disabled by adding the following to your InfluxDB configuration -file: - -``` -[admin] - enabled = false -``` - -### HTTP - -HTTP is required when using the [InfluxDB CLI] or other tools such as Grafana, -thus it should be enabled. When enabling make sure to _also_ enable -authentication: - -``` -[http] - enabled = true - auth-enabled = true -``` - -_**Note:** Before you enable authentication, you might want to [create an -admin user](#create-a-new-admin-user)._ - -### UDP - -GitLab writes data to InfluxDB via UDP and thus this must be enabled. Enabling -UDP can be done using the following settings: - -``` -[[udp]] - enabled = true - bind-address = ":8089" - database = "gitlab" - batch-size = 1000 - batch-pending = 5 - batch-timeout = "1s" - read-buffer = 209715200 -``` - -This does the following: - -1. Enable UDP and bind it to port 8089 for all addresses. -2. Store any data received in the "gitlab" database. -3. Define a batch of points to be 1000 points in size and allow a maximum of - 5 batches _or_ flush them automatically after 1 second. -4. Define a UDP read buffer size of 200 MB. - -One of the most important settings here is the UDP read buffer size as if this -value is set too low, packets will be dropped. You must also make sure the OS -buffer size is set to the same value, the default value is almost never enough. - -To set the OS buffer size to 200 MB, on Linux you can run the following command: - -```bash -sysctl -w net.core.rmem_max=209715200 -``` - -To make this permanent, add the following to `/etc/sysctl.conf` and restart the -server: - -```bash -net.core.rmem_max=209715200 -``` - -It is **very important** to make sure the buffer sizes are large enough to -handle all data sent to InfluxDB as otherwise you _will_ lose data. The above -buffer sizes are based on the traffic for GitLab.com. Depending on the amount of -traffic, users may be able to use a smaller buffer size, but we highly recommend -using _at least_ 100 MB. - -When enabling UDP, users should take care to not expose the port to the public, -as doing so will allow anybody to write data into your InfluxDB database (as -[InfluxDB's UDP protocol][udp] doesn't support authentication). We recommend either -whitelisting the allowed IP addresses/ranges, or setting up a VLAN and only -allowing traffic from members of said VLAN. - -## Create a new admin user - -If you want to [enable authentication](#http), you might want to [create an -admin user][influx-admin]: - -``` -influx -execute "CREATE USER thedude WITH PASSWORD '1234' WITH ALL PRIVILEGES" -``` - -## Create the `gitlab` database - -Once you get InfluxDB up and running, you need to create a database for GitLab. -Make sure you have changed the [storage engine](#storage-engine) to `tsm1` -before creating a database. - -_**Note:** If you [created an admin user](#create-a-new-admin-user) and enabled -[HTTP authentication](#http), remember to append the username (`-username thedude`) -and password (`-password 1234`) to the commands below._ - -Run the following command to create a database named `gitlab`: - -```bash -influx -execute 'CREATE DATABASE gitlab' -``` - -The name **must** be `gitlab`, do not use any other name. - -Next, make sure that the database was successfully created: - -```bash -influx -execute 'SHOW DATABASES' -``` - -The output should be similar to: - -``` -name: databases ---------------- -name -_internal -gitlab -``` - -That's it! Now your GitLab instance should send data to InfluxDB. - ---- - -Read more on: - -- [Introduction to GitLab Performance Monitoring](introduction.md) -- [GitLab Configuration](gitlab_configuration.md) -- [InfluxDB Schema](influxdb_schema.md) - -[influxdb-retention]: https://docs.influxdata.com/influxdb/v0.9/query_language/database_management/#retention-policy-management -[influxdb documentation]: https://docs.influxdata.com/influxdb/v0.9/ -[influxdb cli]: https://docs.influxdata.com/influxdb/v0.9/tools/shell/ -[udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/ -[influxdb]: https://influxdata.com/time-series-platform/influxdb/ -[tsm tree]: https://influxdata.com/blog/new-storage-engine-time-structured-merge-tree/ -[tsm1-commit]: https://github.com/influxdata/influxdb/commit/15d723dc77651bac83e09e2b1c94be480966cb0d -[influx-admin]: https://docs.influxdata.com/influxdb/v0.9/administration/authentication_and_authorization/#create-a-new-admin-user diff --git a/doc/integration/metrics/influxdb_schema.md b/doc/integration/metrics/influxdb_schema.md deleted file mode 100644 index a5a8aebd2d1..00000000000 --- a/doc/integration/metrics/influxdb_schema.md +++ /dev/null @@ -1,87 +0,0 @@ -# InfluxDB Schema - -The following measurements are currently stored in InfluxDB: - -- `PROCESS_file_descriptors` -- `PROCESS_gc_statistics` -- `PROCESS_memory_usage` -- `PROCESS_method_calls` -- `PROCESS_object_counts` -- `PROCESS_transactions` -- `PROCESS_views` - -Here, `PROCESS` is replaced with either `rails` or `sidekiq` depending on the -process type. In all series, any form of duration is stored in milliseconds. - -## PROCESS_file_descriptors - -This measurement contains the number of open file descriptors over time. The -value field `value` contains the number of descriptors. - -## PROCESS_gc_statistics - -This measurement contains Ruby garbage collection statistics such as the amount -of minor/major GC runs (relative to the last sampling interval), the time spent -in garbage collection cycles, and all fields/values returned by `GC.stat`. - -## PROCESS_memory_usage - -This measurement contains the process' memory usage (in bytes) over time. The -value field `value` contains the number of bytes. - -## PROCESS_method_calls - -This measurement contains the methods called during a transaction along with -their duration, and a name of the transaction action that invoked the method (if -available). The method call duration is stored in the value field `duration`, -while the method name is stored in the tag `method`. The tag `action` contains -the full name of the transaction action. Both the `method` and `action` fields -are in the following format: - -``` -ClassName#method_name -``` - -For example, a method called by the `show` method in the `UsersController` class -would have `action` set to `UsersController#show`. - -## PROCESS_object_counts - -This measurement is used to store retained Ruby objects (per class) and the -amount of retained objects. The number of objects is stored in the `count` value -field while the class name is stored in the `type` tag. - -## PROCESS_transactions - -This measurement is used to store basic transaction details such as the time it -took to complete a transaction, how much time was spent in SQL queries, etc. The -following value fields are available: - -| Value | Description | -| ----- | ----------- | -| `duration` | The total duration of the transaction | -| `allocated_memory` | The amount of bytes allocated while the transaction was running. This value is only reliable when using single-threaded application servers | -| `method_duration` | The total time spent in method calls | -| `sql_duration` | The total time spent in SQL queries | -| `view_duration` | The total time spent in views | - -## PROCESS_views - -This measurement is used to store view rendering timings for a transaction. The -following value fields are available: - -| Value | Description | -| ----- | ----------- | -| `duration` | The rendering time of the view | -| `view` | The path of the view, relative to the application's root directory | - -The `action` tag contains the action name of the transaction that rendered the -view. - ---- - -Read more on: - -- [Introduction to GitLab Performance Monitoring](introduction.md) -- [GitLab Configuration](gitlab_configuration.md) -- [InfluxDB Configuration](influxdb_configuration.md) diff --git a/doc/integration/metrics/introduction.md b/doc/integration/metrics/introduction.md deleted file mode 100644 index f2460d31302..00000000000 --- a/doc/integration/metrics/introduction.md +++ /dev/null @@ -1,64 +0,0 @@ -# GitLab Performance Monitoring - -GitLab comes with its own application performance measuring system as of GitLab -8.4, simply called "GitLab Performance Monitoring". GitLab Performance Monitoring is available in both the -Community and Enterprise editions. - -Apart from this introduction, you are advised to read through the following -documents in order to understand and properly configure GitLab Performance Monitoring: - -- [GitLab Configuration](gitlab_configuration.md) -- [InfluxDB Configuration](influxdb_configuration.md) -- [InfluxDB Schema](influxdb_schema.md) - -## Introduction to GitLab Performance Monitoring - -GitLab Performance Monitoring makes it possible to measure a wide variety of statistics -including (but not limited to): - -- The time it took to complete a transaction (a web request or Sidekiq job). -- The time spent in running SQL queries and rendering HAML views. -- The time spent executing (instrumented) Ruby methods. -- Ruby object allocations, and retained objects in particular. -- System statistics such as the process' memory usage and open file descriptors. -- Ruby garbage collection statistics. - -Metrics data is written to [InfluxDB][influxdb] over [UDP][influxdb-udp]. Stored -data can be visualized using [Grafana][grafana] or any other application that -supports reading data from InfluxDB. Alternatively data can be queried using the -InfluxDB CLI. - -## Metric Types - -Two types of metrics are collected: - -1. Transaction specific metrics. -1. Sampled metrics, collected at a certain interval in a separate thread. - -### Transaction Metrics - -Transaction metrics are metrics that can be associated with a single -transaction. This includes statistics such as the transaction duration, timings -of any executed SQL queries, time spent rendering HAML views, etc. These metrics -are collected for every Rack request and Sidekiq job processed. - -### Sampled Metrics - -Sampled metrics are metrics that can't be associated with a single transaction. -Examples include garbage collection statistics and retained Ruby objects. These -metrics are collected at a regular interval. This interval is made up out of two -parts: - -1. A user defined interval. -1. A randomly generated offset added on top of the interval, the same offset - can't be used twice in a row. - -The actual interval can be anywhere between a half of the defined interval and a -half above the interval. For example, for a user defined interval of 15 seconds -the actual interval can be anywhere between 7.5 and 22.5. The interval is -re-generated for every sampling run instead of being generated once and re-used -for the duration of the process' lifetime. - -[influxdb]: https://influxdata.com/time-series-platform/influxdb/ -[influxdb-udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/ -[grafana]: http://grafana.org/ diff --git a/doc/monitoring/performance/gitlab_configuration.md b/doc/monitoring/performance/gitlab_configuration.md new file mode 100644 index 00000000000..b856e7935a3 --- /dev/null +++ b/doc/monitoring/performance/gitlab_configuration.md @@ -0,0 +1,39 @@ +# GitLab Configuration + +GitLab Performance Monitoring is disabled by default. To enable it and change any of its +settings, navigate to the Admin area in **Settings > Metrics** +(`/admin/application_settings`). + +The minimum required settings you need to set are the InfluxDB host and port. +Make sure _Enable InfluxDB Metrics_ is checked and hit **Save** to save the +changes. + +--- + +![GitLab Performance Monitoring Admin Settings](img/metrics_gitlab_configuration_settings.png) + +--- + +Finally, a restart of all GitLab processes is required for the changes to take +effect: + +```bash +# For Omnibus installations +sudo gitlab-ctl restart + +# For installations from source +sudo service gitlab restart +``` + +## Pending Migrations + +When any migrations are pending, the metrics are disabled until the migrations +have been performed. + +--- + +Read more on: + +- [Introduction to GitLab Performance Monitoring](introduction.md) +- [InfluxDB Configuration](influxdb_configuration.md) +- [InfluxDB Schema](influxdb_schema.md) diff --git a/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png b/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png new file mode 100644 index 00000000000..14d82b6ac98 Binary files /dev/null and b/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png differ diff --git a/doc/monitoring/performance/influxdb_configuration.md b/doc/monitoring/performance/influxdb_configuration.md new file mode 100644 index 00000000000..df5e4af7fad --- /dev/null +++ b/doc/monitoring/performance/influxdb_configuration.md @@ -0,0 +1,192 @@ +# InfluxDB Configuration + +The default settings provided by [InfluxDB] are not sufficient for a high traffic +GitLab environment. The settings discussed in this document are based on the +settings GitLab uses for GitLab.com, depending on your own needs you may need to +further adjust them. + +If you are intending to run InfluxDB on the same server as GitLab, make sure +you have plenty of RAM since InfluxDB can use quite a bit depending on traffic. + +Unless you are going with a budget setup, it's advised to run it separately. + +## Requirements + +- InfluxDB 0.9.5 or newer +- A fairly modern version of Linux +- At least 4GB of RAM +- At least 10GB of storage for InfluxDB data + +Note that the RAM and storage requirements can differ greatly depending on the +amount of data received/stored. To limit the amount of stored data users can +look into [InfluxDB Retention Policies][influxdb-retention]. + +## Installation + +Installing InfluxDB is out of the scope of this document. Please refer to the +[InfluxDB documentation]. + +## InfluxDB Server Settings + +Since InfluxDB has many settings that users may wish to customize themselves +(e.g. what port to run InfluxDB on), we'll only cover the essentials. + +The configuration file in question is usually located at +`/etc/influxdb/influxdb.conf`. Whenever you make a change in this file, +InfluxDB needs to be restarted. + +### Storage Engine + +InfluxDB comes with different storage engines and as of InfluxDB 0.9.5 a new +storage engine is available, called [TSM Tree]. All users **must** use the new +`tsm1` storage engine as this [will be the default engine][tsm1-commit] in +upcoming InfluxDB releases. + +Make sure you have the following in your configuration file: + +``` +[data] + dir = "/var/lib/influxdb/data" + engine = "tsm1" +``` + +### Admin Panel + +Production environments should have the InfluxDB admin panel **disabled**. This +feature can be disabled by adding the following to your InfluxDB configuration +file: + +``` +[admin] + enabled = false +``` + +### HTTP + +HTTP is required when using the [InfluxDB CLI] or other tools such as Grafana, +thus it should be enabled. When enabling make sure to _also_ enable +authentication: + +``` +[http] + enabled = true + auth-enabled = true +``` + +_**Note:** Before you enable authentication, you might want to [create an +admin user](#create-a-new-admin-user)._ + +### UDP + +GitLab writes data to InfluxDB via UDP and thus this must be enabled. Enabling +UDP can be done using the following settings: + +``` +[[udp]] + enabled = true + bind-address = ":8089" + database = "gitlab" + batch-size = 1000 + batch-pending = 5 + batch-timeout = "1s" + read-buffer = 209715200 +``` + +This does the following: + +1. Enable UDP and bind it to port 8089 for all addresses. +2. Store any data received in the "gitlab" database. +3. Define a batch of points to be 1000 points in size and allow a maximum of + 5 batches _or_ flush them automatically after 1 second. +4. Define a UDP read buffer size of 200 MB. + +One of the most important settings here is the UDP read buffer size as if this +value is set too low, packets will be dropped. You must also make sure the OS +buffer size is set to the same value, the default value is almost never enough. + +To set the OS buffer size to 200 MB, on Linux you can run the following command: + +```bash +sysctl -w net.core.rmem_max=209715200 +``` + +To make this permanent, add the following to `/etc/sysctl.conf` and restart the +server: + +```bash +net.core.rmem_max=209715200 +``` + +It is **very important** to make sure the buffer sizes are large enough to +handle all data sent to InfluxDB as otherwise you _will_ lose data. The above +buffer sizes are based on the traffic for GitLab.com. Depending on the amount of +traffic, users may be able to use a smaller buffer size, but we highly recommend +using _at least_ 100 MB. + +When enabling UDP, users should take care to not expose the port to the public, +as doing so will allow anybody to write data into your InfluxDB database (as +[InfluxDB's UDP protocol][udp] doesn't support authentication). We recommend either +whitelisting the allowed IP addresses/ranges, or setting up a VLAN and only +allowing traffic from members of said VLAN. + +## Create a new admin user + +If you want to [enable authentication](#http), you might want to [create an +admin user][influx-admin]: + +``` +influx -execute "CREATE USER thedude WITH PASSWORD '1234' WITH ALL PRIVILEGES" +``` + +## Create the `gitlab` database + +Once you get InfluxDB up and running, you need to create a database for GitLab. +Make sure you have changed the [storage engine](#storage-engine) to `tsm1` +before creating a database. + +_**Note:** If you [created an admin user](#create-a-new-admin-user) and enabled +[HTTP authentication](#http), remember to append the username (`-username thedude`) +and password (`-password 1234`) to the commands below._ + +Run the following command to create a database named `gitlab`: + +```bash +influx -execute 'CREATE DATABASE gitlab' +``` + +The name **must** be `gitlab`, do not use any other name. + +Next, make sure that the database was successfully created: + +```bash +influx -execute 'SHOW DATABASES' +``` + +The output should be similar to: + +``` +name: databases +--------------- +name +_internal +gitlab +``` + +That's it! Now your GitLab instance should send data to InfluxDB. + +--- + +Read more on: + +- [Introduction to GitLab Performance Monitoring](introduction.md) +- [GitLab Configuration](gitlab_configuration.md) +- [InfluxDB Schema](influxdb_schema.md) + +[influxdb-retention]: https://docs.influxdata.com/influxdb/v0.9/query_language/database_management/#retention-policy-management +[influxdb documentation]: https://docs.influxdata.com/influxdb/v0.9/ +[influxdb cli]: https://docs.influxdata.com/influxdb/v0.9/tools/shell/ +[udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/ +[influxdb]: https://influxdata.com/time-series-platform/influxdb/ +[tsm tree]: https://influxdata.com/blog/new-storage-engine-time-structured-merge-tree/ +[tsm1-commit]: https://github.com/influxdata/influxdb/commit/15d723dc77651bac83e09e2b1c94be480966cb0d +[influx-admin]: https://docs.influxdata.com/influxdb/v0.9/administration/authentication_and_authorization/#create-a-new-admin-user diff --git a/doc/monitoring/performance/influxdb_schema.md b/doc/monitoring/performance/influxdb_schema.md new file mode 100644 index 00000000000..a5a8aebd2d1 --- /dev/null +++ b/doc/monitoring/performance/influxdb_schema.md @@ -0,0 +1,87 @@ +# InfluxDB Schema + +The following measurements are currently stored in InfluxDB: + +- `PROCESS_file_descriptors` +- `PROCESS_gc_statistics` +- `PROCESS_memory_usage` +- `PROCESS_method_calls` +- `PROCESS_object_counts` +- `PROCESS_transactions` +- `PROCESS_views` + +Here, `PROCESS` is replaced with either `rails` or `sidekiq` depending on the +process type. In all series, any form of duration is stored in milliseconds. + +## PROCESS_file_descriptors + +This measurement contains the number of open file descriptors over time. The +value field `value` contains the number of descriptors. + +## PROCESS_gc_statistics + +This measurement contains Ruby garbage collection statistics such as the amount +of minor/major GC runs (relative to the last sampling interval), the time spent +in garbage collection cycles, and all fields/values returned by `GC.stat`. + +## PROCESS_memory_usage + +This measurement contains the process' memory usage (in bytes) over time. The +value field `value` contains the number of bytes. + +## PROCESS_method_calls + +This measurement contains the methods called during a transaction along with +their duration, and a name of the transaction action that invoked the method (if +available). The method call duration is stored in the value field `duration`, +while the method name is stored in the tag `method`. The tag `action` contains +the full name of the transaction action. Both the `method` and `action` fields +are in the following format: + +``` +ClassName#method_name +``` + +For example, a method called by the `show` method in the `UsersController` class +would have `action` set to `UsersController#show`. + +## PROCESS_object_counts + +This measurement is used to store retained Ruby objects (per class) and the +amount of retained objects. The number of objects is stored in the `count` value +field while the class name is stored in the `type` tag. + +## PROCESS_transactions + +This measurement is used to store basic transaction details such as the time it +took to complete a transaction, how much time was spent in SQL queries, etc. The +following value fields are available: + +| Value | Description | +| ----- | ----------- | +| `duration` | The total duration of the transaction | +| `allocated_memory` | The amount of bytes allocated while the transaction was running. This value is only reliable when using single-threaded application servers | +| `method_duration` | The total time spent in method calls | +| `sql_duration` | The total time spent in SQL queries | +| `view_duration` | The total time spent in views | + +## PROCESS_views + +This measurement is used to store view rendering timings for a transaction. The +following value fields are available: + +| Value | Description | +| ----- | ----------- | +| `duration` | The rendering time of the view | +| `view` | The path of the view, relative to the application's root directory | + +The `action` tag contains the action name of the transaction that rendered the +view. + +--- + +Read more on: + +- [Introduction to GitLab Performance Monitoring](introduction.md) +- [GitLab Configuration](gitlab_configuration.md) +- [InfluxDB Configuration](influxdb_configuration.md) diff --git a/doc/monitoring/performance/introduction.md b/doc/monitoring/performance/introduction.md new file mode 100644 index 00000000000..f2460d31302 --- /dev/null +++ b/doc/monitoring/performance/introduction.md @@ -0,0 +1,64 @@ +# GitLab Performance Monitoring + +GitLab comes with its own application performance measuring system as of GitLab +8.4, simply called "GitLab Performance Monitoring". GitLab Performance Monitoring is available in both the +Community and Enterprise editions. + +Apart from this introduction, you are advised to read through the following +documents in order to understand and properly configure GitLab Performance Monitoring: + +- [GitLab Configuration](gitlab_configuration.md) +- [InfluxDB Configuration](influxdb_configuration.md) +- [InfluxDB Schema](influxdb_schema.md) + +## Introduction to GitLab Performance Monitoring + +GitLab Performance Monitoring makes it possible to measure a wide variety of statistics +including (but not limited to): + +- The time it took to complete a transaction (a web request or Sidekiq job). +- The time spent in running SQL queries and rendering HAML views. +- The time spent executing (instrumented) Ruby methods. +- Ruby object allocations, and retained objects in particular. +- System statistics such as the process' memory usage and open file descriptors. +- Ruby garbage collection statistics. + +Metrics data is written to [InfluxDB][influxdb] over [UDP][influxdb-udp]. Stored +data can be visualized using [Grafana][grafana] or any other application that +supports reading data from InfluxDB. Alternatively data can be queried using the +InfluxDB CLI. + +## Metric Types + +Two types of metrics are collected: + +1. Transaction specific metrics. +1. Sampled metrics, collected at a certain interval in a separate thread. + +### Transaction Metrics + +Transaction metrics are metrics that can be associated with a single +transaction. This includes statistics such as the transaction duration, timings +of any executed SQL queries, time spent rendering HAML views, etc. These metrics +are collected for every Rack request and Sidekiq job processed. + +### Sampled Metrics + +Sampled metrics are metrics that can't be associated with a single transaction. +Examples include garbage collection statistics and retained Ruby objects. These +metrics are collected at a regular interval. This interval is made up out of two +parts: + +1. A user defined interval. +1. A randomly generated offset added on top of the interval, the same offset + can't be used twice in a row. + +The actual interval can be anywhere between a half of the defined interval and a +half above the interval. For example, for a user defined interval of 15 seconds +the actual interval can be anywhere between 7.5 and 22.5. The interval is +re-generated for every sampling run instead of being generated once and re-used +for the duration of the process' lifetime. + +[influxdb]: https://influxdata.com/time-series-platform/influxdb/ +[influxdb-udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/ +[grafana]: http://grafana.org/ -- cgit v1.2.1 From 096485ef65ffce7d31d0cefe530cc1b5c1825b52 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Wed, 20 Jan 2016 16:55:47 -0200 Subject: Creator should be added as a master of the project on creation This also enable a project creator to add themselves as a master of the project. --- app/models/member.rb | 7 ++++++- app/services/projects/create_service.rb | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/models/member.rb b/app/models/member.rb index 28aee2e3799..34efcd0088d 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -91,7 +91,7 @@ class Member < ActiveRecord::Base member.invite_email = user end - if can_update_member?(current_user, member) + if can_update_member?(current_user, member) || project_creator?(member, access_level) member.created_by ||= current_user member.access_level = access_level @@ -107,6 +107,11 @@ class Member < ActiveRecord::Base current_user.can?(:update_group_member, member) || current_user.can?(:update_project_member, member) end + + def project_creator?(member, access_level) + member.new_record? && member.owner? && + access_level.to_i == ProjectMember::MASTER + end end def invite? diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index c94d7ab710f..a6820183bee 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -95,7 +95,7 @@ module Projects system_hook_service.execute_hooks_for(@project, :create) unless @project.group - @project.team << [current_user, :master] + @project.team << [current_user, :master, current_user] end @project.import_start if @project.import? -- cgit v1.2.1 From dd59fc213c6cf2a2efb39eda241f453bbd1e82c7 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 20 Jan 2016 23:10:27 +0100 Subject: Change InfluxDB admin username [ci skip] --- doc/monitoring/performance/influxdb_configuration.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/monitoring/performance/influxdb_configuration.md b/doc/monitoring/performance/influxdb_configuration.md index df5e4af7fad..3a2b598b78f 100644 --- a/doc/monitoring/performance/influxdb_configuration.md +++ b/doc/monitoring/performance/influxdb_configuration.md @@ -135,7 +135,7 @@ If you want to [enable authentication](#http), you might want to [create an admin user][influx-admin]: ``` -influx -execute "CREATE USER thedude WITH PASSWORD '1234' WITH ALL PRIVILEGES" +influx -execute "CREATE USER jeff WITH PASSWORD '1234' WITH ALL PRIVILEGES" ``` ## Create the `gitlab` database @@ -145,8 +145,8 @@ Make sure you have changed the [storage engine](#storage-engine) to `tsm1` before creating a database. _**Note:** If you [created an admin user](#create-a-new-admin-user) and enabled -[HTTP authentication](#http), remember to append the username (`-username thedude`) -and password (`-password 1234`) to the commands below._ +[HTTP authentication](#http), remember to append the username (`-username `) +and password (`-password `) you set earlier to the commands below._ Run the following command to create a database named `gitlab`: -- cgit v1.2.1 From c45a6bf3ba13cbd532852dfcc48ef3fd7aa545e4 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 14 Jan 2016 17:52:59 +0100 Subject: Added cache:key to .gitlab-ci.yml allowing to fine tune the caching --- CHANGELOG | 1 + doc/ci/yaml/README.md | 60 ++++++++++++++++++++++++++-- lib/ci/gitlab_ci_yaml_processor.rb | 8 ++++ spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 25 ++++++++++-- 4 files changed, 86 insertions(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9490540f1b1..e7c7e65ac4a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -74,6 +74,7 @@ v 8.4.0 (unreleased) - Fix: Creator should be added as a master of the project on creation - Added X-GitLab-... headers to emails from CI and Email On Push services (Anton Baklanov) - Add IP check against DNSBLs at account sign-up + - Added cache:key to .gitlab-ci.yml allowing to fine tune the caching v 8.3.4 - Use gitlab-workhorse 0.5.4 (fixes API routing bug) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index fd0d49de4e4..3b594df659d 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -135,10 +135,9 @@ thus allowing to fine tune them. ### cache `cache` is used to specify a list of files and directories which should be -cached between builds. Caches are stored according to the branch/ref and the -job name. They are not currently shared between different job names or between -branches/refs, which means that caching will benefit you if you push subsequent -commits to an existing feature branch. +cached between builds. + +**By default the caching is enabled per-job and per-branch.** If `cache` is defined outside the scope of the jobs, it means it is set globally and all jobs will use its definition. @@ -152,6 +151,59 @@ cache: - binaries/ ``` +#### cache:key + +_**Note:** Introduced in GitLab Runner v1.0.0._ + +The `key` directive allows you to define the affinity of caching +between jobs, allowing to have a single cache for all jobs, +cache per-job, cache per-branch or any other way you deem proper. + +This allows you to fine tune caching, allowing you to cache data between different jobs or even different branches. +The `cache:key` variable can use any of the [predefined variables](../variables/README.md): + +Example configurations: + +To enable per-job caching: + + ```yaml + cache: + key: "$CI_BUILD_NAME" + untracked: true + ``` + +To enable per-branch caching: + + ```yaml + cache: + key: "$CI_BUILD_REF_NAME" + untracked: true + ``` + +To enable per-job and per-branch caching: + + ```yaml + cache: + key: "$CI_BUILD_NAME/$CI_BUILD_REF_NAME" + untracked: true + ``` + +To enable per-branch and per-stage caching: + + ```yaml + cache: + key: "$CI_BUILD_STAGE/$CI_BUILD_REF_NAME" + untracked: true + ``` + +If you use **Windows Batch** to run your shell scripts you need to replace the `$` with `%`: + + ```yaml + cache: + key: "%CI_BUILD_STAGE%/%CI_BUILD_REF_NAME%" + untracked: true + ``` + ## Jobs `.gitlab-ci.yml` allows you to specify an unlimited number of jobs. Each job diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index bcdfd38d292..1a3f662811a 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -115,6 +115,10 @@ module Ci end if @cache + if @cache[:key] && !validate_string(@cache[:key]) + raise ValidationError, "cache:key parameter should be a string" + end + if @cache[:untracked] && !validate_boolean(@cache[:untracked]) raise ValidationError, "cache:untracked parameter should be an boolean" end @@ -198,6 +202,10 @@ module Ci end def validate_job_cache!(name, job) + if job[:cache][:key] && !validate_string(job[:cache][:key]) + raise ValidationError, "#{name} job: cache:key parameter should be a string" + end + if job[:cache][:untracked] && !validate_boolean(job[:cache][:untracked]) raise ValidationError, "#{name} job: cache:untracked parameter should be an boolean" end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index d15100fc6d8..f3394910c5b 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -336,7 +336,7 @@ module Ci describe "Caches" do it "returns cache when defined globally" do config = YAML.dump({ - cache: { paths: ["logs/", "binaries/"], untracked: true }, + cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, rspec: { script: "rspec" } @@ -348,13 +348,14 @@ module Ci expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( paths: ["logs/", "binaries/"], untracked: true, + key: 'key', ) end it "returns cache when defined in a job" do config = YAML.dump({ rspec: { - cache: { paths: ["logs/", "binaries/"], untracked: true }, + cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, script: "rspec" } }) @@ -365,15 +366,16 @@ module Ci expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( paths: ["logs/", "binaries/"], untracked: true, + key: 'key', ) end it "overwrite cache when defined for a job and globally" do config = YAML.dump({ - cache: { paths: ["logs/", "binaries/"], untracked: true }, + cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' }, rspec: { script: "rspec", - cache: { paths: ["test/"], untracked: false }, + cache: { paths: ["test/"], untracked: false, key: 'local' }, } }) @@ -383,6 +385,7 @@ module Ci expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( paths: ["test/"], untracked: false, + key: 'local', ) end end @@ -615,6 +618,20 @@ module Ci end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:paths parameter should be an array of strings") end + it "returns errors if cache:key is not a string" do + config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:key parameter should be a string") + end + + it "returns errors if job cache:key is not an a string" do + config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { key: 1 } } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:key parameter should be a string") + end + it "returns errors if job cache:untracked is not an array of strings" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { untracked: "string" } } }) expect do -- cgit v1.2.1 From dfdf189135971917fcd0425c53dd27009833f1be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 20 Jan 2016 23:24:13 +0100 Subject: Fix indentation of methods chaining in Coffee [ci skip] --- app/assets/javascripts/notes.js.coffee | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index fd6f89be98d..53d72be66e3 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -131,8 +131,8 @@ class @Notes @note_ids.push(note.id) $('ul.main-notes-list') - .append(note.html) - .syntaxHighlight() + .append(note.html) + .syntaxHighlight() @initTaskList() @updateNotesCount(1) @@ -178,8 +178,8 @@ class @Notes # Init discussion on 'Discussion' page if it is merge request page if $('body').attr('data-page').indexOf('projects:merge_request') is 0 $('ul.main-notes-list') - .append(note.discussion_with_diff_html) - .syntaxHighlight() + .append(note.discussion_with_diff_html) + .syntaxHighlight() else # append new note to all matching discussions discussionContainer.append note_html @@ -367,8 +367,8 @@ class @Notes ### removeNote: (e) => noteId = $(e.currentTarget) - .closest(".note") - .attr("id") + .closest(".note") + .attr("id") # A same note appears in the "Discussion" and in the "Changes" tab, we have # to remove all. Using $(".note[id='noteId']") ensure we get all the notes, @@ -561,5 +561,5 @@ class @Notes updateTaskList: -> $('form', this).submit() - updateNotesCount: (incrementStep) -> - @notesCountBadge.text parseInt(@notesCountBadge.text()) + incrementStep + updateNotesCount: (updateCount) -> + @notesCountBadge.text(parseInt(@notesCountBadge.text()) + updateCount) -- cgit v1.2.1 From 4d345bc4003022ca22b5e7f42069c5a6bde41b6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 20 Jan 2016 17:29:53 -0500 Subject: Remember last sort option used. --- app/controllers/application_controller.rb | 22 +++++++++++++++++++++- app/models/concerns/cookie_helpers.rb | 5 +++++ app/models/group.rb | 3 ++- app/models/project.rb | 1 + app/models/user.rb | 1 + 5 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 app/models/concerns/cookie_helpers.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 633c3f55614..4c4e2ac3d3c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -297,7 +297,8 @@ class ApplicationController < ActionController::Base end def set_filters_params - params[:sort] ||= 'id_desc' + set_default_sort + params[:scope] = 'all' if params[:scope].blank? params[:state] = 'opened' if params[:state].blank? @@ -404,4 +405,23 @@ class ApplicationController < ActionController::Base current_user.nil? && root_path == request.path end + + private + + def set_default_sort + controller_name = params[:controller].sub(/Controller\Z/, '').underscore + cookie_suffix = "_sort_#{controller_name}" + + key = if @project + "#{@project.cookie_key}#{cookie_suffix}" + elsif @group + "#{@group.cookie_key}#{cookie_suffix}" + else + "#{current_user.cookie_key}#{cookie_suffix}" + end + + cookies[key] ||= 'id_desc' + cookies[key] = params[:sort] if params[:sort].present? + params[:sort] = cookies[key] + end end diff --git a/app/models/concerns/cookie_helpers.rb b/app/models/concerns/cookie_helpers.rb new file mode 100644 index 00000000000..1c8d43a4661 --- /dev/null +++ b/app/models/concerns/cookie_helpers.rb @@ -0,0 +1,5 @@ +module CookieHelpers + def cookie_key + "#{model_name.singular}_#{id}" + end +end diff --git a/app/models/group.rb b/app/models/group.rb index 5a31b46920c..3ba18b85193 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -19,7 +19,8 @@ require 'file_size_validator' class Group < Namespace include Gitlab::ConfigHelper include Referable - + include CookieHelpers + has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember' alias_method :members, :group_members has_many :users, through: :group_members diff --git a/app/models/project.rb b/app/models/project.rb index 5579710a476..5ba7532c61a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -51,6 +51,7 @@ class Project < ActiveRecord::Base include AfterCommitQueue include CaseSensitivity include TokenAuthenticatable + include CookieHelpers extend Gitlab::ConfigHelper diff --git a/app/models/user.rb b/app/models/user.rb index 4214f01f6a4..c24f8a35380 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -73,6 +73,7 @@ class User < ActiveRecord::Base include Sortable include CaseSensitivity include TokenAuthenticatable + include CookieHelpers add_authentication_token_field :authentication_token -- cgit v1.2.1 From 3ae0ca8941603d974c3242881d4248b9c8af00da Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 20 Jan 2016 17:47:18 -0500 Subject: Update CHANGELOG [ci skip] --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 9490540f1b1..704950231d4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,7 @@ v 8.4.0 (unreleased) - Autocomplete data is now always loaded, instead of when focusing a comment text area - Improved performance of finding issues for an entire group - Added custom application performance measuring system powered by InfluxDB + - Add syntax highlighting to diffs - Gracefully handle invalid UTF-8 sequences in Markdown links (Stan Hu) - Bump fog to 1.36.0 (Stan Hu) - Add user's last used IP addresses to admin page (Stan Hu) -- cgit v1.2.1 From 3a6fac4a852c96391ba9f1eea57a873f1a13e12f Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 20 Jan 2016 18:00:29 -0500 Subject: Remove Performance Monitoring from Integration docs [ci skip] --- doc/integration/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/integration/README.md b/doc/integration/README.md index 297e4f6e7bf..5edac746c7b 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -15,7 +15,6 @@ See the documentation below for details on how to configure these services. - [OAuth2 provider](oauth_provider.md) OAuth2 application creation - [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages - [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users -- [GitLab Performance Monitoring](metrics/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics GitLab Enterprise Edition contains [advanced Jenkins support][jenkins]. -- cgit v1.2.1 From 8367e4abebcf23a6be830db57193417d3efe20b5 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 20 Jan 2016 09:42:13 -0800 Subject: Implement new UI for pagination --- CHANGELOG | 1 + app/assets/stylesheets/framework/pagination.scss | 29 +++------------------- .../framework/tw_bootstrap_variables.scss | 22 ++++++++-------- app/views/kaminari/gitlab/_next_page.html.haml | 8 ++++-- app/views/kaminari/gitlab/_paginator.html.haml | 4 +-- app/views/kaminari/gitlab/_prev_page.html.haml | 8 ++++-- app/views/projects/issues/_issues.html.haml | 5 ---- .../merge_requests/_merge_requests.html.haml | 5 ---- config/locales/en.yml | 4 +++ 9 files changed, 33 insertions(+), 53 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 704950231d4..79d4b5f94c8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.5.0 (unreleased) - Add "visibility" flag to GET /projects api endpoint - Upgrade gitlab_git to 7.2.23 to fix commit message mentions in first branch push + - New UI for pagination v 8.4.0 (unreleased) - Allow LDAP users to change their email if it was not set by the LDAP server diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss index 2cd30491bf5..8a5bde83335 100644 --- a/app/assets/stylesheets/framework/pagination.scss +++ b/app/assets/stylesheets/framework/pagination.scss @@ -1,35 +1,12 @@ .gl-pagination { + text-align: center; + width: 100%; border-top: 1px solid $border-color; - background-color: $background-color; - margin: -$gl-padding; + margin: 0; margin-top: 0; .pagination { padding: 0; - margin: 0; - display: block; - - li.first, - li.last, - li.next, - li.prev { - > a { - color: $link-color; - - &:hover { - color: #fff; - } - } - } - - li > a, - li > span { - border: none; - margin: 0; - @include border-radius(0 !important); - padding: 13px 19px; - border-right: 1px solid $border-color; - } } } diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index cd0621cdbf3..798cd224ad0 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -66,20 +66,20 @@ $legend-color: $text-color; //## $pagination-color: $gl-gray; -$pagination-bg: $background-color; -$pagination-border: transparent; +$pagination-bg: #fff; +$pagination-border: $border-color; -$pagination-hover-color: #fff; -$pagination-hover-bg: $brand-info; -$pagination-hover-border: transparent; +$pagination-hover-color: $gl-gray; +$pagination-hover-bg: $hover; +$pagination-hover-border: $border-color; -$pagination-active-color: #fff; -$pagination-active-bg: $brand-info; -$pagination-active-border: transparent; +$pagination-active-color: $blue-dark; +$pagination-active-bg: #fff; +$pagination-active-border: $border-color; -$pagination-disabled-color: #fff; -$pagination-disabled-bg: lighten($brand-info, 15%); -$pagination-disabled-border: transparent; +$pagination-disabled-color: #cdcdcd; +$pagination-disabled-bg: $background-color; +$pagination-disabled-border: $border-color; //== Form states and alerts diff --git a/app/views/kaminari/gitlab/_next_page.html.haml b/app/views/kaminari/gitlab/_next_page.html.haml index 00c5f0b6f4e..c805914fc3f 100644 --- a/app/views/kaminari/gitlab/_next_page.html.haml +++ b/app/views/kaminari/gitlab/_next_page.html.haml @@ -5,5 +5,9 @@ -# num_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%li.next - = link_to_unless current_page.last?, raw(t 'views.pagination.next'), url, rel: 'next', remote: remote +- if current_page.last? + %li{ class: "next disabled" } + %span= raw(t 'views.pagination.next') +- else + %li{ class: "next" } + = link_to raw(t 'views.pagination.next'), url, rel: 'next', remote: remote diff --git a/app/views/kaminari/gitlab/_paginator.html.haml b/app/views/kaminari/gitlab/_paginator.html.haml index 2f645186921..a12c53bcfe7 100644 --- a/app/views/kaminari/gitlab/_paginator.html.haml +++ b/app/views/kaminari/gitlab/_paginator.html.haml @@ -10,13 +10,13 @@ %ul.pagination.clearfix - unless current_page.first? = first_page_tag unless num_pages < 5 # As kaminari will always show the first 5 pages - = prev_page_tag + = prev_page_tag - each_page do |page| - if page.left_outer? || page.right_outer? || page.inside_window? = page_tag page - elsif !page.was_truncated? = gap_tag + = next_page_tag - unless current_page.last? - = next_page_tag = last_page_tag unless num_pages < 5 diff --git a/app/views/kaminari/gitlab/_prev_page.html.haml b/app/views/kaminari/gitlab/_prev_page.html.haml index f673abdb3ae..afb20455e0a 100644 --- a/app/views/kaminari/gitlab/_prev_page.html.haml +++ b/app/views/kaminari/gitlab/_prev_page.html.haml @@ -5,5 +5,9 @@ -# num_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%li{class: "prev" } - = link_to_unless current_page.first?, raw(t 'views.pagination.previous'), url, rel: 'prev', remote: remote +- if current_page.first? + %li{ class: "prev disabled" } + %span= raw(t 'views.pagination.previous') +- else + %li{ class: "prev" } + = link_to raw(t 'views.pagination.previous'), url, rel: 'prev', remote: remote diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml index e0e89b764d5..f34f3c05737 100644 --- a/app/views/projects/issues/_issues.html.haml +++ b/app/views/projects/issues/_issues.html.haml @@ -5,9 +5,4 @@ .nothing-here-block No issues to show - if @issues.present? - .issuable-filter-count - %span.pull-right - = number_with_delimiter(@issues.total_count) - issues for this filter - = paginate @issues, theme: "gitlab" diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml index 29d09d0a652..5473fa19166 100644 --- a/app/views/projects/merge_requests/_merge_requests.html.haml +++ b/app/views/projects/merge_requests/_merge_requests.html.haml @@ -5,10 +5,5 @@ .nothing-here-block No merge requests to show - if @merge_requests.present? - .issuable-filter-count - %span.pull-right - = number_with_delimiter(@merge_requests.total_count) - merge requests for this filter - = paginate @merge_requests, theme: "gitlab" diff --git a/config/locales/en.yml b/config/locales/en.yml index f6cfb5efd2a..cedb5e207bd 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -8,3 +8,7 @@ en: wrong_size: "is the wrong size (should be %{file_size})" size_too_small: "is too small (should be at least %{file_size})" size_too_big: "is too big (should be at most %{file_size})" + views: + pagination: + previous: "Prev" + next: "Next" -- cgit v1.2.1 From ceab12866f55b422bee4ad3d6828d08abce8b874 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Wed, 20 Jan 2016 21:25:26 -0500 Subject: Removes 100% width because it is a block element. --- app/assets/stylesheets/framework/pagination.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss index 8a5bde83335..b6f21fd8c91 100644 --- a/app/assets/stylesheets/framework/pagination.scss +++ b/app/assets/stylesheets/framework/pagination.scss @@ -1,6 +1,5 @@ .gl-pagination { text-align: center; - width: 100%; border-top: 1px solid $border-color; margin: 0; margin-top: 0; -- cgit v1.2.1 From fe4affcc3ff9c315e95a0ac36513e1558c5c8b43 Mon Sep 17 00:00:00 2001 From: Pirate Praveen Date: Tue, 15 Dec 2015 06:04:53 -0500 Subject: add execution permission for script --- Rakefile | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 Rakefile diff --git a/Rakefile b/Rakefile old mode 100644 new mode 100755 -- cgit v1.2.1 From 33a5bbcf2f4fe9cf4ea1faf7bf6aeae5fc2810d5 Mon Sep 17 00:00:00 2001 From: Pirate Praveen Date: Thu, 21 Jan 2016 01:29:16 -0500 Subject: remove execution permissions from fonts --- app/assets/fonts/OFL.txt | 0 app/assets/fonts/SourceSansPro-Black.ttf.woff | Bin app/assets/fonts/SourceSansPro-Black.ttf.woff2 | Bin app/assets/fonts/SourceSansPro-BlackIt.ttf.woff | Bin app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2 | Bin app/assets/fonts/SourceSansPro-Bold.ttf.woff | Bin app/assets/fonts/SourceSansPro-Bold.ttf.woff2 | Bin app/assets/fonts/SourceSansPro-BoldIt.ttf.woff | Bin app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2 | Bin app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff | Bin app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2 | Bin app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff | Bin app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2 | Bin app/assets/fonts/SourceSansPro-It.ttf.woff | Bin app/assets/fonts/SourceSansPro-It.ttf.woff2 | Bin app/assets/fonts/SourceSansPro-Light.ttf.woff | Bin app/assets/fonts/SourceSansPro-Light.ttf.woff2 | Bin app/assets/fonts/SourceSansPro-LightIt.ttf.woff | Bin app/assets/fonts/SourceSansPro-LightIt.ttf.woff2 | Bin app/assets/fonts/SourceSansPro-Regular.ttf.woff | Bin app/assets/fonts/SourceSansPro-Regular.ttf.woff2 | Bin app/assets/fonts/SourceSansPro-Semibold.ttf.woff | Bin app/assets/fonts/SourceSansPro-Semibold.ttf.woff2 | Bin app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff | Bin app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2 | Bin 25 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 app/assets/fonts/OFL.txt mode change 100755 => 100644 app/assets/fonts/SourceSansPro-Black.ttf.woff mode change 100755 => 100644 app/assets/fonts/SourceSansPro-Black.ttf.woff2 mode change 100755 => 100644 app/assets/fonts/SourceSansPro-BlackIt.ttf.woff mode change 100755 => 100644 app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2 mode change 100755 => 100644 app/assets/fonts/SourceSansPro-Bold.ttf.woff mode change 100755 => 100644 app/assets/fonts/SourceSansPro-Bold.ttf.woff2 mode change 100755 => 100644 app/assets/fonts/SourceSansPro-BoldIt.ttf.woff mode change 100755 => 100644 app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2 mode change 100755 => 100644 app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff mode change 100755 => 100644 app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2 mode change 100755 => 100644 app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff mode change 100755 => 100644 app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2 mode change 100755 => 100644 app/assets/fonts/SourceSansPro-It.ttf.woff mode change 100755 => 100644 app/assets/fonts/SourceSansPro-It.ttf.woff2 mode change 100755 => 100644 app/assets/fonts/SourceSansPro-Light.ttf.woff mode change 100755 => 100644 app/assets/fonts/SourceSansPro-Light.ttf.woff2 mode change 100755 => 100644 app/assets/fonts/SourceSansPro-LightIt.ttf.woff mode change 100755 => 100644 app/assets/fonts/SourceSansPro-LightIt.ttf.woff2 mode change 100755 => 100644 app/assets/fonts/SourceSansPro-Regular.ttf.woff mode change 100755 => 100644 app/assets/fonts/SourceSansPro-Regular.ttf.woff2 mode change 100755 => 100644 app/assets/fonts/SourceSansPro-Semibold.ttf.woff mode change 100755 => 100644 app/assets/fonts/SourceSansPro-Semibold.ttf.woff2 mode change 100755 => 100644 app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff mode change 100755 => 100644 app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2 diff --git a/app/assets/fonts/OFL.txt b/app/assets/fonts/OFL.txt old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-Black.ttf.woff b/app/assets/fonts/SourceSansPro-Black.ttf.woff old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-Black.ttf.woff2 b/app/assets/fonts/SourceSansPro-Black.ttf.woff2 old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2 old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf.woff b/app/assets/fonts/SourceSansPro-Bold.ttf.woff old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf.woff2 b/app/assets/fonts/SourceSansPro-Bold.ttf.woff2 old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2 old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2 b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2 old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2 old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-It.ttf.woff b/app/assets/fonts/SourceSansPro-It.ttf.woff old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-It.ttf.woff2 b/app/assets/fonts/SourceSansPro-It.ttf.woff2 old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-Light.ttf.woff b/app/assets/fonts/SourceSansPro-Light.ttf.woff old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-Light.ttf.woff2 b/app/assets/fonts/SourceSansPro-Light.ttf.woff2 old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2 old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf.woff b/app/assets/fonts/SourceSansPro-Regular.ttf.woff old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf.woff2 b/app/assets/fonts/SourceSansPro-Regular.ttf.woff2 old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2 b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2 old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff old mode 100755 new mode 100644 diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2 old mode 100755 new mode 100644 -- cgit v1.2.1 From f121d2d8f841dcc430eb2d30007fd506a1a06a04 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 20 Jan 2016 23:48:56 -0800 Subject: Fix Error 500 when doing a search for merge requests Potential fix for #11547 --- app/views/search/results/_merge_request.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml index 2efa616d664..faeb2b55c6f 100644 --- a/app/views/search/results/_merge_request.html.haml +++ b/app/views/search/results/_merge_request.html.haml @@ -6,7 +6,7 @@ - if merge_request.description.present? .description.term = preserve do - = search_md_sanitize(markdown(merge_request.description)) + = search_md_sanitize(markdown(merge_request.description, { project: merge_request.project })) %span.light #{merge_request.project.name_with_namespace} .pull-right -- cgit v1.2.1 From 82571eab43606071f61d5dcf82df488bf6cefc6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 21 Jan 2016 10:08:12 +0100 Subject: Fix failing specs --- features/steps/shared/diff_note.rb | 12 ++++++------ spec/features/notes_on_merge_requests_spec.rb | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index c6a0ae2ba38..06e69441894 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -23,7 +23,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[rel$='#{sample_commit.line_code}']") do + page.within("form[id$='#{sample_commit.line_code}']") do fill_in "note[note]", with: "Typo, please fix" find(".js-comment-button").trigger("click") sleep 0.05 @@ -33,7 +33,7 @@ module SharedDiffNote step 'I leave a diff comment in a parallel view on the left side like "Old comment"' do click_parallel_diff_line(sample_commit.line_code, 'old') - page.within("#{diff_file_selector} form[rel$='#{sample_commit.line_code}']") do + page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}']") do fill_in "note[note]", with: "Old comment" find(".js-comment-button").trigger("click") end @@ -41,7 +41,7 @@ module SharedDiffNote step 'I leave a diff comment in a parallel view on the right side like "New comment"' do click_parallel_diff_line(sample_commit.line_code, 'new') - page.within("#{diff_file_selector} form[rel$='#{sample_commit.line_code}']") do + page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}']") do fill_in "note[note]", with: "New comment" find(".js-comment-button").trigger("click") end @@ -51,7 +51,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[rel$='#{sample_commit.line_code}']") do + page.within("form[id$='#{sample_commit.line_code}']") do fill_in "note[note]", with: "Should fix it :smile:" find('.js-md-preview-button').click end @@ -62,7 +62,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.del_line_code) - page.within("form[rel$='#{sample_commit.del_line_code}']") do + page.within("form[id$='#{sample_commit.del_line_code}']") do fill_in "note[note]", with: "DRY this up" find('.js-md-preview-button').click end @@ -91,7 +91,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[rel$='#{sample_commit.line_code}']") do + page.within("form[id$='#{sample_commit.line_code}']") do fill_in 'note[note]', with: ':smile:' click_button('Add Comment') end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index f0fc6916c4d..1a360cd1ebc 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -167,7 +167,7 @@ describe 'Comments', feature: true do end it 'should be removed when canceled' do - page.within(".diff-file form[rel$='#{line_code}']") do + page.within(".diff-file form[id$='#{line_code}']") do find('.js-close-discussion-note-form').trigger('click') end -- cgit v1.2.1 From 7d1b51fac1c518dec5c33d30c4cc4e5bffdc127e Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 21 Jan 2016 11:27:31 +0100 Subject: Instrument Gitlab::Git::Repository This adds instrumentation for the instance methods of Gitlab::Git::Repository. --- config/initializers/metrics.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb index b1fe36dc21c..0945b93ed5d 100644 --- a/config/initializers/metrics.rb +++ b/config/initializers/metrics.rb @@ -49,6 +49,7 @@ if Gitlab::Metrics.enabled? config.instrument_instance_methods(Gitlab::Shell) config.instrument_methods(Gitlab::Git) + config.instrument_instance_methods(Gitlab::Git::Repository) Gitlab::Git.constants.each do |name| const = Gitlab::Git.const_get(name) -- cgit v1.2.1 From 693fa7a458645291cc903bf400f9b463941cb63d Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 21 Jan 2016 13:36:55 +0100 Subject: Init script changes for gitlab-workhorse in 8.4 --- lib/support/init.d/gitlab | 9 ++++++--- lib/support/init.d/gitlab.default.example | 3 +++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab index c5f07c8b508..1633891c8a0 100755 --- a/lib/support/init.d/gitlab +++ b/lib/support/init.d/gitlab @@ -38,6 +38,7 @@ web_server_pid_path="$pid_path/unicorn.pid" sidekiq_pid_path="$pid_path/sidekiq.pid" mail_room_enabled=false mail_room_pid_path="$pid_path/mail_room.pid" +gitlab_workhorse_dir=$(cd $app_root/../gitlab-workhorse && pwd) gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid" gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $rails_socket -documentRoot $app_root/public" gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log" @@ -233,10 +234,12 @@ start_gitlab() { if [ "$gitlab_workhorse_status" = "0" ]; then echo "The gitlab-workhorse is already running with pid $spid, not restarting" else - # No need to remove a socket, gitlab-workhorse does this itself + # No need to remove a socket, gitlab-workhorse does this itself. + # Because gitlab-workhorse has multiple executables we need to fix + # the PATH. $app_root/bin/daemon_with_pidfile $gitlab_workhorse_pid_path \ - $app_root/../gitlab-workhorse/gitlab-workhorse \ - $gitlab_workhorse_options \ + /usr/bin/env PATH=$gitlab_workhorse_dir:$PATH \ + gitlab-workhorse $gitlab_workhorse_options \ >> $gitlab_workhorse_log 2>&1 & fi diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example index 1937ca582b0..4e6e56ac2db 100755 --- a/lib/support/init.d/gitlab.default.example +++ b/lib/support/init.d/gitlab.default.example @@ -30,6 +30,9 @@ web_server_pid_path="$pid_path/unicorn.pid" # The default is "$pid_path/sidekiq.pid" sidekiq_pid_path="$pid_path/sidekiq.pid" +# The directory where the gitlab-workhorse binaries are. Usually +# /home/git/gitlab-workhorse . +gitlab_workhorse_dir=$(cd $app_root/../gitlab-workhorse && pwd) gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid" # The -listenXxx settings determine where gitlab-workhorse # listens for connections from NGINX. To listen on localhost:8181, write -- cgit v1.2.1 From 21facf535377ad58d07e8943033fe14efface8b2 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 21 Jan 2016 14:23:53 +0100 Subject: Use consistent markup and styling for highlighting across blobs, diffs, blame and snippets --- app/assets/stylesheets/framework/common.scss | 8 --- app/assets/stylesheets/framework/files.scss | 21 +++----- app/assets/stylesheets/highlight/dark.scss | 49 +++++++++--------- app/assets/stylesheets/highlight/monokai.scss | 49 +++++++++--------- .../stylesheets/highlight/solarized_dark.scss | 49 +++++++++--------- .../stylesheets/highlight/solarized_light.scss | 50 +++++++++--------- app/assets/stylesheets/highlight/white.scss | 60 ++++++++++++++++++---- app/assets/stylesheets/pages/diff.scss | 47 ++--------------- app/assets/stylesheets/pages/note_form.scss | 12 ----- app/assets/stylesheets/pages/notes.scss | 3 -- app/models/members/project_member.rb | 6 +-- app/views/projects/blame/show.html.haml | 17 +++--- app/views/projects/blob/diff.html.haml | 3 +- app/views/projects/blob/preview.html.haml | 10 ++-- app/views/projects/diffs/_match_line.html.haml | 2 +- .../projects/diffs/_match_line_parallel.html.haml | 8 +-- app/views/projects/diffs/_parallel_view.html.haml | 16 +++--- app/views/projects/diffs/_text_file.html.haml | 6 +-- .../projects/notes/discussions/_diff.html.haml | 10 ++-- app/views/search/results/_snippet_blob.html.haml | 42 ++++++++------- app/views/shared/_file_highlight.html.haml | 4 +- 21 files changed, 212 insertions(+), 260 deletions(-) diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 585a9d83913..9bc814cfd2d 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -120,14 +120,6 @@ span.update-author { display: inline; } -.line_holder { - &:hover { - td { - background: #FFFFCF !important; - } - } -} - p.time { color: #999; font-size: 90%; diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 6ee104ee31a..a4791cf6b34 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -92,15 +92,6 @@ &:last-child { border-right: none; } - background: #fff; - } - .lines { - pre { - padding: 0; - margin: 0; - background: none; - border: none; - } } img.avatar { border: 0 none; @@ -116,18 +107,18 @@ color: #888; } } - td.blame-numbers { - pre { - color: #AAA; - white-space: pre; - } - background: #f1f1f1; + td.line-numbers { + float: none; border-left: 1px solid #DDD; } td.lines { + padding: 0; code { font-family: $monospace_font; } + pre { + margin: 0; + } } } diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index 8201735beb5..3ba2cc94ad2 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -1,20 +1,35 @@ /* https://github.com/MozMorris/tomorrow-pygments */ .code.dark { + // Line numbers + .line-numbers, .diff-line-num { + background-color: #1d1f21; + } - background-color: #1d1f21 !important; - color: #c5c8c6 !important; - - pre.highlight, - .line-numbers, - .line-numbers a { - background-color: #1d1f21 !important; - color: #c5c8c6 !important; + .diff-line-num, .diff-line-num a { + color: rgba(255, 255, 255, 0.3); } + // Code itself pre.code { border-left: 1px solid #666; } + &, pre.code, .line_holder .line_content { + background-color: #1d1f21; + color: #c5c8c6; + } + + // Diff line + .line_holder { + .diff-line-num.new, .line_content.new { + @include diff_background(51, 255, 51, #808080); + } + + .diff-line-num.old, .line_content.old { + @include diff_background(255, 51, 51, #808080); + } + } + // highlight line via anchor pre .hll { background-color: #557 !important; @@ -90,22 +105,4 @@ .vg { color: #cc6666 } /* Name.Variable.Global */ .vi { color: #cc6666 } /* Name.Variable.Instance */ .il { color: #de935f } /* Literal.Number.Integer.Long */ - - .line_holder { - &.parallel .new.new_line, - &.parallel .new.line_content, - &.new .old_line, - &.new .new_line, - &.new .line_content { - @include diff_background(255, 255, 255, #808080); - } - - &.parallel .old.old_line, - &.parallel .old.line_content, - &.old .old_line, - &.old .new_line, - &.old .line_content { - @include diff_background(255, 51, 51, #808080); - } - } } diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index cc03ed6ae45..b47aef73d2b 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -1,20 +1,35 @@ /* https://github.com/richleland/pygments-css/blob/master/monokai.css */ .code.monokai { + // Line numbers + .line-numbers, .diff-line-num { + background-color: #272822; + } - background-color: #272822 !important; - color: #f8f8f2 !important; - - pre.highlight, - .line-numbers, - .line-numbers a { - background-color :#272822 !important; - color: #f8f8f2 !important; + .diff-line-num, .diff-line-num a { + color: #f8f8f2; } + // Code itself pre.code { border-left: 1px solid #555; } + &, pre.code, .line_holder .line_content { + background-color: #272822; + color: #f8f8f2; + } + + // Diff line + .line_holder { + .diff-line-num.new, .line_content.new { + @include diff_background(156, 175, 183, #808080); + } + + .diff-line-num.old, .line_content.old { + @include diff_background(254, 147, 140, #808080); + } + } + // highlight line via anchor pre .hll { background-color: #49483e !important; @@ -90,22 +105,4 @@ .gu { color: #75715e; } /* Generic.Subheading & Diff Unified/Comment? */ .gd { color: #f92672; } /* Generic.Deleted & Diff Deleted */ .gi { color: #a6e22e; } /* Generic.Inserted & Diff Inserted */ - - .line_holder { - &.parallel .new.new_line, - &.parallel .new.line_content, - &.new .old_line, - &.new .new_line, - &.new .line_content { - @include diff_background(156, 175, 183, #808080); - } - - &.parallel .old.old_line, - &.parallel .old.line_content, - &.old .old_line, - &.old .new_line, - &.old .line_content { - @include diff_background(254, 147, 140, #808080); - } - } } diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index fdfac6cd249..0aab580bd3c 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -1,20 +1,35 @@ /* https://gist.github.com/qguv/7936275 */ .code.solarized-dark { + // Line numbers + .line-numbers, .diff-line-num { + background-color: #002b36; + } - background-color: #002b36 !important; - color: #93a1a1 !important; - - pre.highlight, - .line-numbers, - .line-numbers a { - background-color: #002b36 !important; - color: #93a1a1 !important; + .diff-line-num, .diff-line-num a { + color: #93a1a1; } + // Code itself pre.code { border-left: 1px solid #113b46; } + &, pre.code, .line_holder .line_content { + background-color: #002b36; + color: #93a1a1; + } + + // Diff line + .line_holder { + .diff-line-num.new, .line_content.new { + @include diff_background(255, 255, 255, #808080); + } + + .diff-line-num.old, .line_content.old { + @include diff_background(255, 51, 51, #808080); + } + } + // highlight line via anchor pre .hll { background-color: #174652 !important; @@ -111,22 +126,4 @@ .vg { color: #268bd2 } /* Name.Variable.Global */ .vi { color: #268bd2 } /* Name.Variable.Instance */ .il { color: #2aa198 } /* Literal.Number.Integer.Long */ - - .line_holder { - &.parallel .new.new_line, - &.parallel .new.line_content, - &.new .old_line, - &.new .new_line, - &.new .line_content { - @include diff_background(255, 255, 255, #808080); - } - - &.parallel .old.old_line, - &.parallel .old.line_content, - &.old .old_line, - &.old .new_line, - &.old .line_content { - @include diff_background(255, 51, 51, #808080); - } - } } diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index f9788951aa8..3aaadcd0f96 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -1,20 +1,35 @@ /* https://gist.github.com/qguv/7936275 */ .code.solarized-light { + // Line numbers + .line-numbers, .diff-line-num { + background-color: #fdf6e3; + } - background-color: #fdf6e3 !important; - color: #586e75 !important; - - pre.highlight, - .line-numbers, - .line-numbers a { - background-color: #fdf6e3 !important; - color: #586e75 !important; + .diff-line-num, .diff-line-num a { + color: #586e75; } + // Code itself pre.code { border-left: 1px solid #c5d0d4; } + &, pre.code, .line_holder .line_content { + background-color: #fdf6e3; + color: #586e75; + } + + // Diff line + .line_holder { + .diff-line-num.new, .line_content.new { + @include diff_background(92, 164, 169, #FAF3DD); + } + + .diff-line-num.old, .line_content.old { + @include diff_background(237, 106, 90, #FAF3DD); + } + } + // highlight line via anchor pre .hll { background-color: #ddd8c5 !important; @@ -111,23 +126,4 @@ .vg { color: #268bd2 } /* Name.Variable.Global */ .vi { color: #268bd2 } /* Name.Variable.Instance */ .il { color: #2aa198 } /* Literal.Number.Integer.Long */ - - - .line_holder { - &.parallel .new.new_line, - &.parallel .new.line_content, - &.new .old_line, - &.new .new_line, - &.new .line_content { - @include diff_background(92, 164, 169, #FAF3DD); - } - - &.parallel .old.old_line, - &.parallel .old.line_content, - &.old .old_line, - &.old .new_line, - &.old .line_content { - @include diff_background(237, 106, 90, #FAF3DD); - } - } } diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index e2626da7871..8a932e6540e 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -1,20 +1,60 @@ /* https://github.com/aahan/pygments-github-style */ .code.white { + // Line numbers + .line-numbers, .diff-line-num { + background-color: $background-color; + } - background-color: #f8fafc !important; - color: #5b6169 !important; - - pre.highlight, - .line-numbers, - .line-numbers a { - background-color: $background-color !important; - color: $gl-gray !important; + .diff-line-num, .diff-line-num a { + color: rgba(0, 0, 0, 0.3); } + // Code itself pre.code { border-left: 1px solid $border-color; - background-color: #fff !important; - color: #333 !important; + } + + &, pre.code, .line_holder .line_content { + background-color: #fff; + color: #333; + } + + // Diff line + .line_holder { + .diff-line-num { + &.old { + background: #ffdddd; + border-color: #f1c0c0; + } + + &.new { + background: #dbffdb; + border-color: #c1e9c1; + } + } + + .line_content { + &.old { + background: #ffecec; + + span.idiff { + background-color: #f8cbcb; + } + } + + &.new { + background: #eaffea; + + span.idiff { + background-color: #a6f3a6; + } + } + + &.match { + color: rgba(0, 0, 0, 0.3); + background: #fafafa; + } + } } // highlight line via anchor diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 0b79aa172a4..06d0ece1c66 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -32,16 +32,6 @@ background: #FFF; color: #333; - .old { - span.idiff { - background-color: #f8cbcb; - } - } - .new { - span.idiff { - background-color: #a6f3a6; - } - } .unfold { cursor: pointer; } @@ -76,7 +66,7 @@ } tr.line_holder.parallel { - .old_line, .new_line, .diff_line { + .old_line, .new_line { min-width: 50px; } @@ -85,7 +75,7 @@ } } - .old_line, .new_line, .diff_line { + .old_line, .new_line { margin: 0px; padding: 0px; border: none; @@ -107,43 +97,12 @@ text-decoration: underline; } } - &.new { - background: #CFD; - } - &.old { - background: #FDD; - } - } - .diff_line { - padding: 0; - } - .line_holder { - &.old .old_line, - &.old .new_line { - background: #ffdddd; - border-color: #f1c0c0; - } - &.new .old_line, - &.new .new_line { - background: #dbffdb; - border-color: #c1e9c1; - } } .line_content { display: block; margin: 0px; padding: 0px 0.5em; border: none; - &.new { - background: #eaffea; - } - &.old { - background: #ffecec; - } - &.matched { - color: $border-color; - background: #fafafa; - } &.parallel { display: table-cell; } @@ -400,7 +359,7 @@ /* RGBa with 0.3 opacity */ background: rgba($r, $g, $b, 0.3); - &.new_line, &.old_line { + &.diff-line-num { border-right-color: $custom-border !important; } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 2c9a42f9892..32ba1676333 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -10,18 +10,6 @@ margin: 10px $gl-padding; } .diff-file .diff-content { - tr.line_holder:hover { - &> td.line_content { - background: $hover !important; - border-color: darken($hover, 10%) !important; - } - &> td.new_line, - &> td.old_line { - background: darken($hover, 4%) !important; - border-color: darken($hover, 10%) !important; - } - } - tr.line_holder:hover > td .line_note_link { opacity: 1.0; filter: alpha(opacity=100); diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 72b0ed29a69..f6343e6bb1e 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -242,11 +242,8 @@ ul.notes { // "show" the icon also if we just hover somewhere over the line &:hover > td { - background: $hover !important; - .add-diff-note { @include show-add-diff-note; } } } - diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 1b0c76917aa..560d1690e14 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -84,7 +84,7 @@ class ProjectMember < Member def truncate_teams(project_ids) ProjectMember.transaction do members = ProjectMember.where(source_id: project_ids) - + members.each do |member| member.destroy end @@ -133,13 +133,13 @@ class ProjectMember < Member event_service.join_project(self.project, self.user) notification_service.new_project_member(self) end - + super end def post_update_hook if access_level_changed? - notification_service.update_project_member(self) + notification_service.update_project_member(self) end super diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index d5d04954490..53dcac78a9f 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -12,7 +12,7 @@ %small= number_to_human_size @blob.size .file-actions = render "projects/blob/actions" - .file-content.blame.highlight + .file-content.blame.code.js-syntax-highlight %table - current_line = 1 - blame_highlighter = highlighter(@blob.name, @blob.data, nowrap: true) @@ -31,15 +31,14 @@ = commit_author_link(commit, avatar: false) authored #{time_ago_with_tooltip(commit.committed_date, skip_js: true)} - %td.lines.blame-numbers - %pre - - line_count = blame_group[:lines].count - - (current_line...(current_line + line_count)).each do |i| - = i - \ - - current_line += line_count + %td.line-numbers + - line_count = blame_group[:lines].count + - (current_line...(current_line + line_count)).each do |i| + %a.diff-line-num= i + \ + - current_line += line_count %td.lines - %pre{class: 'code highlight white'} + %pre{class: 'code highlight'} %code - blame_group[:lines].each do |line| :preserve diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index 2e913802be1..abcfca4cd11 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -10,7 +10,8 @@ %tr.line_holder %td.old_line.diff-line-num{data: {linenumber: line_old}} = link_to raw(line_old), "#" - %td.new_line= link_to raw(line_new) , "#" + %td.new_line.diff-line-num + = link_to raw(line_new) , "#" %td.line_content.noteable_line==#{' ' * @form.indent}#{line} - if @form.unfold? && @form.bottom? && @form.to < @blob.loc diff --git a/app/views/projects/blob/preview.html.haml b/app/views/projects/blob/preview.html.haml index fed483d6788..c5a269f334c 100644 --- a/app/views/projects/blob/preview.html.haml +++ b/app/views/projects/blob/preview.html.haml @@ -14,12 +14,12 @@ - @diff_lines.each do |line| %tr.line_holder{ class: "#{line.type}" } - if line.type == "match" - %td.old_line= "..." - %td.new_line= "..." - %td.line_content.matched= line.text + %td.old_line.diff-line-num= "..." + %td.new_line.diff-line-num= "..." + %td.line_content.match= line.text - else - %td.old_line - %td.new_line + %td.old_line.diff-line-num + %td.new_line.diff-line-num %td.line_content{class: "#{line.type}"}= diff_line_content(line.text) - else .nothing-here-block No changes. diff --git a/app/views/projects/diffs/_match_line.html.haml b/app/views/projects/diffs/_match_line.html.haml index d1f897b99f7..d6dddd97879 100644 --- a/app/views/projects/diffs/_match_line.html.haml +++ b/app/views/projects/diffs/_match_line.html.haml @@ -4,4 +4,4 @@ %td.new_line.diff-line-num{data: {linenumber: line_new}, class: [unfold_bottom_class(bottom), unfold_class(!new_file)]} \... -%td.line_content.matched= line +%td.line_content.match= line diff --git a/app/views/projects/diffs/_match_line_parallel.html.haml b/app/views/projects/diffs/_match_line_parallel.html.haml index 815df16aa4a..0cd888876e0 100644 --- a/app/views/projects/diffs/_match_line_parallel.html.haml +++ b/app/views/projects/diffs/_match_line_parallel.html.haml @@ -1,4 +1,4 @@ -%td.old_line - %td.line_content.parallel.matched= line -%td.new_line - %td.line_content.parallel.matched= line +%td.old_line.diff-line-num +%td.line_content.parallel.match= line +%td.new_line.diff-line-num +%td.line_content.parallel.match= line diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index a15d147ab05..d7c49068745 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -9,16 +9,16 @@ = render "projects/diffs/match_line_parallel", { line: left[:text], line_old: left[:number], line_new: right[:number] } - elsif left[:type] == 'nonewline' - %td.old_line - %td.line_content.parallel.matched= left[:text] - %td.new_line - %td.line_content.parallel.matched= left[:text] + %td.old_line.diff-line-num + %td.line_content.parallel.match= left[:text] + %td.new_line.diff-line-num + %td.line_content.parallel.match= left[:text] - else - %td.old_line{id: left[:line_code], class: "#{left[:type]}"} + %td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]}"} = link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code] - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(left[:line_code], 'old') - %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text]) + %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text]) - if right[:type] == 'new' - new_line_class = 'new' @@ -27,11 +27,11 @@ - new_line_class = nil - new_line_code = left[:line_code] - %td.new_line{id: new_line_code, class: "#{new_line_class}", data: { linenumber: right[:number] }} + %td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class}", data: { linenumber: right[:number] }} = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(right[:line_code], 'new') - %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", data: { line_code: new_line_code }}= diff_line_content(right[:text]) + %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", data: { line_code: new_line_code }}= diff_line_content(right[:text]) - if @reply_allowed - comments_left, comments_right = organize_comments(left[:type], right[:type], left[:line_code], right[:line_code]) diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index f4fc6caba0f..d9f2dce1389 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -18,13 +18,13 @@ - elsif type == 'nonewline' %td.old_line.diff-line-num %td.new_line.diff-line-num - %td.line_content.matched= line.text + %td.line_content.match= line.text - else - %td.old_line + %td.old_line.diff-line-num{class: type} = link_to raw(type == "new" ? " " : line_old), "##{line_code}", id: line_code - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code) - %td.new_line{data: {linenumber: line.new_pos}} + %td.new_line.diff-line-num{class: type, data: {linenumber: line.new_pos}} = link_to raw(type == "old" ? " " : line.new_pos), "##{line_code}", id: line_code %td.line_content{class: "noteable_line #{type} #{line_code}", data: { line_code: line_code }}= diff_line_content(line.text) diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml index 46962b184ce..92bbaf0961a 100644 --- a/app/views/projects/notes/discussions/_diff.html.haml +++ b/app/views/projects/notes/discussions/_diff.html.haml @@ -16,13 +16,13 @@ - line_code = generate_line_code(note.file_path, line) %tr.line_holder{ id: line_code, class: "#{type}" } - if type == "match" - %td.old_line= "..." - %td.new_line= "..." - %td.line_content.matched= line.text + %td.old_line.diff-line-num= "..." + %td.new_line.diff-line-num= "..." + %td.line_content.match= line.text - else - %td.old_line + %td.old_line.diff-line-num = raw(type == "new" ? " " : line.old_pos) - %td.new_line + %td.new_line.diff-line-num = raw(type == "old" ? " " : line.new_pos) %td.line_content{class: "noteable_line #{type} #{line_code}", line_code: line_code}= diff_line_content(line.text) diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index 9a4f9fb9485..dcd61199717 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -22,29 +22,27 @@ .file-content.code .nothing-here-block Empty file - else - .file-content.code - %div.highlighted-data{ class: user_color_scheme } - .line-numbers + .file-content.code.js-syntax-highlight + .line-numbers + - snippet_blob[:snippet_chunks].each do |snippet| + - unless snippet[:data].empty? + - snippet[:data].lines.to_a.size.times do |index| + - offset = defined?(snippet[:start_line]) ? snippet[:start_line] : 1 + - i = index + offset + = link_to snippet_path+"#L#{i}", id: "L#{i}", rel: "#L#{i}", class: "diff-line-num" do + %i.fa.fa-link + = i + - unless snippet == snippet_blob[:snippet_chunks].last + %a.diff-line-num + = "." + %pre.code + %code - snippet_blob[:snippet_chunks].each do |snippet| - unless snippet[:data].empty? - - snippet[:data].lines.to_a.size.times do |index| - - offset = defined?(snippet[:start_line]) ? snippet[:start_line] : 1 - - i = index + offset - = link_to snippet_path+"#L#{i}", id: "L#{i}", rel: "#L#{i}" do - %i.fa.fa-link - = i + = snippet[:data] - unless snippet == snippet_blob[:snippet_chunks].last %a - = "." - .highlight.term - %pre - %code - - snippet_blob[:snippet_chunks].each do |snippet| - - unless snippet[:data].empty? - = snippet[:data] - - unless snippet == snippet_blob[:snippet_chunks].last - %a - = "..." - - else - .file-content.code - .nothing-here-block Empty file + = "..." + - else + .file-content.code + .nothing-here-block Empty file diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml index 1bef1de433a..ee242c94db8 100644 --- a/app/views/shared/_file_highlight.html.haml +++ b/app/views/shared/_file_highlight.html.haml @@ -1,11 +1,11 @@ -.file-content.code.js-syntax-highlight{ class: user_color_scheme } +.file-content.code.js-syntax-highlight .line-numbers - if blob.data.present? - blob.data.lines.each_index do |index| - offset = defined?(first_line_number) ? first_line_number : 1 - i = index + offset -# We're not using `link_to` because it is too slow once we get to thousands of lines. - %a{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i} + %a.diff-line-num{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i} %i.fa.fa-link = i .blob-content{data: {blob_id: blob.id}} -- cgit v1.2.1 From ad6b6b9ede9b8652dd5479009734cc0a462a65b1 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 21 Jan 2016 14:24:40 +0100 Subject: Mention init script update --- doc/update/8.3-to-8.4.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/doc/update/8.3-to-8.4.md b/doc/update/8.3-to-8.4.md index 604939cd733..5f1372e7725 100644 --- a/doc/update/8.3-to-8.4.md +++ b/doc/update/8.3-to-8.4.md @@ -104,10 +104,7 @@ via [/etc/default/gitlab]. #### Init script -We updated the init script for GitLab in order to pass new -configuration options to gitlab-workhorse. We let gitlab-workhorse -connect to the Rails application via a Unix domain socket and we tell -it where the 'public' directory of GitLab is. +We updated the init script for GitLab in order to set a specific PATH for gitlab-workhorse. ``` cd /home/git/gitlab -- cgit v1.2.1 From 1c1590a510de0e6836e204b6d47fa810e5ec4f6c Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 21 Jan 2016 14:44:56 +0100 Subject: Use gitlab-workhorse 0.6.1 --- GITLAB_WORKHORSE_VERSION | 2 +- doc/update/8.3-to-8.4.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index a918a2aa18d..ee6cdce3c29 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -0.6.0 +0.6.1 diff --git a/doc/update/8.3-to-8.4.md b/doc/update/8.3-to-8.4.md index 5f1372e7725..bf80f66d004 100644 --- a/doc/update/8.3-to-8.4.md +++ b/doc/update/8.3-to-8.4.md @@ -48,7 +48,7 @@ which should already be on your system from GitLab 8.1. ```bash cd /home/git/gitlab-workhorse sudo -u git -H git fetch --all -sudo -u git -H git checkout 0.6.0 +sudo -u git -H git checkout 0.6.1 sudo -u git -H make ``` -- cgit v1.2.1 From b2c071fad4b7767e1e2e140cf3b3e4c6621c25a1 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 21 Jan 2016 14:56:23 +0100 Subject: Pretty highlighted diffs for every theme --- app/assets/stylesheets/highlight/dark.scss | 9 +++++++-- app/assets/stylesheets/highlight/monokai.scss | 11 ++++++++--- app/assets/stylesheets/highlight/solarized_dark.scss | 11 ++++++++--- app/assets/stylesheets/highlight/solarized_light.scss | 11 ++++++++--- app/assets/stylesheets/pages/diff.scss | 15 ++++++--------- 5 files changed, 37 insertions(+), 20 deletions(-) diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index 3ba2cc94ad2..8464727de9d 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -22,11 +22,16 @@ // Diff line .line_holder { .diff-line-num.new, .line_content.new { - @include diff_background(51, 255, 51, #808080); + @include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.2), #808080); } .diff-line-num.old, .line_content.old { - @include diff_background(255, 51, 51, #808080); + @include diff_background(rgba(255, 51, 51, 0.2), rgba(255, 51, 51, 0.2), #808080); + } + + .line_content.match { + color: rgba(255, 255, 255, 0.3); + background: rgba(255, 255, 255, 0.1); } } diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index b47aef73d2b..516d8aef960 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -6,7 +6,7 @@ } .diff-line-num, .diff-line-num a { - color: #f8f8f2; + color: rgba(255, 255, 255, 0.3); } // Code itself @@ -22,11 +22,16 @@ // Diff line .line_holder { .diff-line-num.new, .line_content.new { - @include diff_background(156, 175, 183, #808080); + @include diff_background(rgba(166, 226, 46, 0.2), rgba(166, 226, 46, 0.3), #808080); } .diff-line-num.old, .line_content.old { - @include diff_background(254, 147, 140, #808080); + @include diff_background(rgba(254, 147, 140, 0.2), rgba(254, 147, 140, 0.3), #808080); + } + + .line_content.match { + color: rgba(255, 255, 255, 0.3); + background: rgba(255, 255, 255, 0.1); } } diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index 0aab580bd3c..ae7ecc65c39 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -6,7 +6,7 @@ } .diff-line-num, .diff-line-num a { - color: #93a1a1; + color: rgba(255, 255, 255, 0.3); } // Code itself @@ -22,11 +22,16 @@ // Diff line .line_holder { .diff-line-num.new, .line_content.new { - @include diff_background(255, 255, 255, #808080); + @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.3), #808080); } .diff-line-num.old, .line_content.old { - @include diff_background(255, 51, 51, #808080); + @include diff_background(rgba(220, 50, 47, 0.2), rgba(220, 50, 47, 0.3), #808080); + } + + .line_content.match { + color: rgba(255, 255, 255, 0.3); + background: rgba(255, 255, 255, 0.1); } } diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index 3aaadcd0f96..1c138572145 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -6,7 +6,7 @@ } .diff-line-num, .diff-line-num a { - color: #586e75; + color: rgba(0, 0, 0, 0.3); } // Code itself @@ -22,11 +22,16 @@ // Diff line .line_holder { .diff-line-num.new, .line_content.new { - @include diff_background(92, 164, 169, #FAF3DD); + @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.3), #FAF3DD); } .diff-line-num.old, .line_content.old { - @include diff_background(237, 106, 90, #FAF3DD); + @include diff_background(rgba(220, 50, 47, 0.2), rgba(220, 50, 47, 0.3), #FAF3DD); + } + + .line_content.match { + color: rgba(0, 0, 0, 0.3); + background: rgba(255, 255, 255, 0.4); } } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 06d0ece1c66..5215df04a6e 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -353,17 +353,14 @@ } } -@mixin diff_background($r, $g, $b, $custom-border) { - /* Fallback for web browsers that doesn't support RGBa */ - background: rgb($r, $g, $b); - /* RGBa with 0.3 opacity */ - background: rgba($r, $g, $b, 0.3); +@mixin diff_background($background, $idiff, $border) { + background: $background; - &.diff-line-num { - border-right-color: $custom-border !important; + &.line_content span.idiff { + background: $idiff; } - &.line_content span.idiff { - background: rgb($r, $g, $b); + &.diff-line-num { + border-color: $border; } } -- cgit v1.2.1 From e5232ccc1ec44ae62b05f80f728b21ee7460b79d Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 21 Jan 2016 15:05:51 +0100 Subject: Do not use progress bar when downloading build artifacts Closes #12491 --- app/views/projects/artifacts/browse.html.haml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index d3c969cc035..8a6fb785708 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -4,7 +4,7 @@ .top-block.gray-content-block.clearfix .pull-right = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), - class: 'btn btn-default' do + class: 'btn btn-default download' do = icon('download') Download artifacts archive @@ -22,6 +22,10 @@ .center Empty :javascript + $('.top-block').on('click', '.download', function(e) { + e.stopPropagation(); + }); + $('.tree-holder').on('click', 'tr[data-link] a', function(e) { e.stopImmediatePropagation(); }); -- cgit v1.2.1 From 85fd6c861fbff3fd0b2d6fdb135b335ece635254 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 21 Jan 2016 15:07:21 +0100 Subject: Make dark inline diffs a little more obvious --- app/assets/stylesheets/highlight/dark.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index 8464727de9d..c7f7a5aaf07 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -22,11 +22,11 @@ // Diff line .line_holder { .diff-line-num.new, .line_content.new { - @include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.2), #808080); + @include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.3), #808080); } .diff-line-num.old, .line_content.old { - @include diff_background(rgba(255, 51, 51, 0.2), rgba(255, 51, 51, 0.2), #808080); + @include diff_background(rgba(255, 51, 51, 0.2), rgba(255, 51, 51, 0.3), #808080); } .line_content.match { -- cgit v1.2.1 From ad9799bdb0232bf3899b184a47f644e13a7dd031 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 21 Jan 2016 14:17:59 +0000 Subject: Milestone drag & drop cursor Fixes #4097 --- app/assets/stylesheets/framework/jquery.scss | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss index 871b808bad4..d6cd78813c0 100644 --- a/app/assets/stylesheets/framework/jquery.scss +++ b/app/assets/stylesheets/framework/jquery.scss @@ -53,3 +53,14 @@ color: #333; } } + +.ui-sortable-handle { + cursor: move; + cursor: -webkit-grab; + cursor: -moz-grab; + + &:active { + cursor: -webkit-grabbing; + cursor: -moz-grabbing; + } +} -- cgit v1.2.1 From 66b0927bb5e19f8a4a3df3e7e401ddd1e3d8e768 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 21 Jan 2016 15:51:44 +0100 Subject: Move build artifacts javascripts into separate file --- app/assets/javascripts/build_artifacts.js.coffee | 14 ++++++++++++++ app/assets/javascripts/dispatcher.js.coffee | 2 ++ app/views/projects/artifacts/browse.html.haml | 13 ------------- 3 files changed, 16 insertions(+), 13 deletions(-) create mode 100644 app/assets/javascripts/build_artifacts.js.coffee diff --git a/app/assets/javascripts/build_artifacts.js.coffee b/app/assets/javascripts/build_artifacts.js.coffee new file mode 100644 index 00000000000..5ae6cba56c8 --- /dev/null +++ b/app/assets/javascripts/build_artifacts.js.coffee @@ -0,0 +1,14 @@ +class @BuildArtifacts + constructor: () -> + @disablePropagation() + @setupEntryClick() + + disablePropagation: -> + $('.top-block').on 'click', '.download', (e) -> + e.stopPropagation() + $('.tree-holder').on 'click', 'tr[data-link] a', (e) -> + e.stopImmediatePropagation() + + setupEntryClick: -> + $('.tree-holder').on 'click', 'tr[data-link]', (e) -> + window.location = @dataset.link diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 0d88e8d254a..2cdf01d874c 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -100,6 +100,8 @@ class Dispatcher shortcut_handler = true when 'projects:forks:new' new ProjectFork() + when 'projects:artifacts:browse' + new BuildArtifacts() when 'users:show' new User() new Activities() diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index 8a6fb785708..84034c8bf16 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -20,16 +20,3 @@ - if @entry.empty? .center Empty - -:javascript - $('.top-block').on('click', '.download', function(e) { - e.stopPropagation(); - }); - - $('.tree-holder').on('click', 'tr[data-link] a', function(e) { - e.stopImmediatePropagation(); - }); - - $('.tree-holder').on('click', 'tr[data-link]', function(e) { - window.location = this.dataset.link; - }); -- cgit v1.2.1 From 4861b1bbfbd962416452e1e883c67b885678b5a0 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 21 Jan 2016 13:11:38 -0200 Subject: Fix import of GitHub's wiki when the repository has not been created --- lib/gitlab/github_import/importer.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 18929b9113b..663402e8197 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -82,8 +82,12 @@ module Gitlab end true - rescue Gitlab::Shell::Error - false + rescue Gitlab::Shell::Error => e + if e.message =~ /repository not exported/ + true + else + false + end end end end -- cgit v1.2.1 From 1c51fb1778e5e14f245f9e8d8a3531a4effc811f Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 21 Jan 2016 16:12:49 +0100 Subject: update Ruby version to 2.2.4 (latest previous stable) and update docs --- .ruby-version | 2 +- doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md | 6 +++--- doc/ci/yaml/README.md | 2 +- doc/development/ci_setup.md | 2 +- doc/install/installation.md | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.ruby-version b/.ruby-version index 04b10b4f150..530cdd91a20 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.1.7 +2.2.4 diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md index e52e1547461..c1bb47e4291 100644 --- a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md +++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md @@ -56,12 +56,12 @@ gitlab-ci-multi-runner register \ --non-interactive \ --url "https://gitlab.com/ci/" \ --registration-token "PROJECT_REGISTRATION_TOKEN" \ - --description "ruby-2.1" \ + --description "ruby-2.2" \ --executor "docker" \ - --docker-image ruby:2.1 \ + --docker-image ruby:2.2 \ --docker-postgres latest ``` -With the command above, you create a runner that uses [ruby:2.1](https://registry.hub.docker.com/u/library/ruby/) image and uses [postgres](https://registry.hub.docker.com/u/library/postgres/) database. +With the command above, you create a runner that uses [ruby:2.2](https://registry.hub.docker.com/u/library/ruby/) image and uses [postgres](https://registry.hub.docker.com/u/library/postgres/) database. To access PostgreSQL database you need to connect to `host: postgres` as user `postgres` without password. diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index fd0d49de4e4..779b9db6486 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -33,7 +33,7 @@ The YAML syntax allows for using more complex job specifications than in the above example: ```yaml -image: ruby:2.1 +image: ruby:2.2 services: - postgres diff --git a/doc/development/ci_setup.md b/doc/development/ci_setup.md index f9b48868182..05db30b4a7e 100644 --- a/doc/development/ci_setup.md +++ b/doc/development/ci_setup.md @@ -26,7 +26,7 @@ We use [these build scripts](https://gitlab.com/gitlab-org/gitlab-ci/blob/master # Build configuration on [Semaphore](https://semaphoreapp.com/gitlabhq/gitlabhq/) for testing the [GitHub.com repo](https://github.com/gitlabhq/gitlabhq) - Language: Ruby -- Ruby version: 2.1.2 +- Ruby version: 2.2.4 - database.yml: pg Build commands diff --git a/doc/install/installation.md b/doc/install/installation.md index 00030729a4b..376a02d4534 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -116,9 +116,9 @@ Remove the old Ruby 1.8 if present Download Ruby and compile it: mkdir /tmp/ruby && cd /tmp/ruby - curl -O --progress https://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.7.tar.gz - echo 'e2e195a4a58133e3ad33b955c829bb536fa3c075 ruby-2.1.7.tar.gz' | shasum -c - && tar xzf ruby-2.1.7.tar.gz - cd ruby-2.1.7 + curl -O --progress https://cache.ruby-lang.org/pub/ruby/2.2/ruby-2.2.4.tar.gz + echo 'e2e195a4a58133e3ad33b955c829bb536fa3c075 ruby-2.2.4.tar.gz' | shasum -c - && tar xzf ruby-2.2.4.tar.gz + cd ruby-2.2.4 ./configure --disable-install-rdoc make sudo make install -- cgit v1.2.1 From d21d8e57d345cf783a7e66fd2a20dae6ec1dff84 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 21 Jan 2016 17:26:44 +0100 Subject: Updated gitlab_git to 7.2.24 Performance of Gitlab::Git::Repository was improved in merge request gitlab-org/gitlab_git!62. --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 5e0718af70f..47b6f4852c7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -356,7 +356,7 @@ GEM posix-spawn (~> 0.3) gitlab_emoji (0.2.0) gemojione (~> 2.1) - gitlab_git (7.2.23) + gitlab_git (7.2.24) activesupport (~> 4.0) charlock_holmes (~> 0.7.3) github-linguist (~> 4.7.0) -- cgit v1.2.1 From 93b942ec7c9dfc5f2f117d1d2161f69e9a39e16f Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 21 Jan 2016 17:27:23 +0100 Subject: Refactor build artifacts documentation [ci skip] --- doc/README.md | 2 +- doc/ci/README.md | 2 +- doc/ci/artifacts/README.md | 50 ------ doc/ci/build_artifacts/README.md | 173 +++++++++++++++++++++ .../img/build_artifacts_browser.png | Bin 0 -> 89132 bytes .../img/build_artifacts_browser_button.png | Bin 0 -> 11614 bytes 6 files changed, 175 insertions(+), 52 deletions(-) delete mode 100644 doc/ci/artifacts/README.md create mode 100644 doc/ci/build_artifacts/README.md create mode 100644 doc/ci/build_artifacts/img/build_artifacts_browser.png create mode 100644 doc/ci/build_artifacts/img/build_artifacts_browser_button.png diff --git a/doc/README.md b/doc/README.md index d75c1fbfe79..f3836d32e20 100644 --- a/doc/README.md +++ b/doc/README.md @@ -30,7 +30,7 @@ - [User permissions](ci/permissions/README.md) - [API](ci/api/README.md) - [Triggering builds through the API](ci/triggers/README.md) -- [Build artifacts](ci/artifacts/README.md) +- [Build artifacts](ci/build_artifacts/README.md) ### CI Languages diff --git a/doc/ci/README.md b/doc/ci/README.md index 19d08721467..5886829be51 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -12,7 +12,7 @@ * [Using Variables](variables/README.md) * [Using SSH keys](ssh_keys/README.md) * [Triggering builds through the API](triggers/README.md) -* [Build artifacts](artifacts/README.md) +* [Build artifacts](build_artifacts/README.md) ### Languages diff --git a/doc/ci/artifacts/README.md b/doc/ci/artifacts/README.md deleted file mode 100644 index 1166304f33c..00000000000 --- a/doc/ci/artifacts/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# Build artifacts - -Since version 8.2 of GitLab and version 0.7.0 of GitLab Runner, build artifacts -created by GitLab Runner are uploaded to GitLab, and then you can download -artifacts archive using GitLab UI. - -Since version 8.4 of GitLab and version 1.0 of GitLab Runner artifacts are -compressed using ZIP format and it is possible to browse content of such an -archive using GitLab UI, and then download a single file from inside it. - -## Artifacts in .gitlab-ci.yml - -Please look into `.gitlab-ci.yml` [documentation](../yaml/README.md). - -## Artifacts archive format - -Prior to version 8.4 of GitLab and 1.0 of GitLab Runner, build artifacts were -compressed using `tar.gz` format. - -Since then, we use a ZIP format. - -## How build artifacts are stored - -After a successful build, GitLab Runner uploads an archive containing build -artifacts to GitLab. This archive is not extracted after that, so its save a -storage space. - -## How do we access content of an artifacts archive - -When GitLab receives an artifacts archive, archive metadata file is being -generated. Metadata file describes all entries that are located in artifacts -archive. This file is in a binary format, with additional GZIP compression. - -It is possible then to browse artifacts using GitLab UI and artifacts browser. - -TODO IMG - -GitLab does not extract artifacts archive to make it possible to browse it. We -use artifacts metadata file instead that contains are relevant information. -This is especially important when there is a lot of artifacts, or an archive is -a very large file. - -## How do we make files downloadable - -When user clicks a regular file, then download of this particular file starts. -GitLab does not extract entire artifacts archive to send a single file to user. - -Instead of extracting entire file, only one file is being extracted. It is not -necessary to extract large archive, just to download a small file that is -inside. diff --git a/doc/ci/build_artifacts/README.md b/doc/ci/build_artifacts/README.md new file mode 100644 index 00000000000..b02caa9edff --- /dev/null +++ b/doc/ci/build_artifacts/README.md @@ -0,0 +1,173 @@ +# Introduction to build artifacts + +Artifacts is a list of files and directories which are attached to a build +after it completes successfully. + +Since GitLab 8.2 and [GitLab Runner] 0.7.0, build artifacts that are created by +GitLab Runner are uploaded to GitLab and are downloadable as a single archive +(`tar.gz`) using the GitLab UI. + +Starting from GitLab 8.4 and GitLab Runner 1.0, the artifacts archive format +changed to `ZIP`, and it is now possible to browse its contents, with the added +ability of downloading the files separately. + +## Enabling build artifacts + +If you are searching for ways to use the artifacts feature, jump to +[Defining artifacts in `.gitlab-ci.yml`](#defining-artifacts-in-gitlab-ciyml). + +The artifacts feature is enabled by default in all GitLab installations. + +If by any chance you want to disable the artifacts feature on your GitLab +instance, follow the steps below. + +--- + +**In Omnibus installations:** + +1. Edit `/etc/gitlab/gitlab.rb` and add the following line: + + ```ruby + gitlab_rails['artifacts_enabled'] = false + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +--- + +**In installations from source:** + +1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines: + + ```yaml + artifacts: + enabled: false + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. + +## Defining artifacts in `.gitlab-ci.yml` + +A simple example of using the artifacts definition in `.gitlab-ci.yml` would be +the following: + +```yaml +pdf: + script: xelatex mycv.tex + artifacts: + paths: + - mycv.pdf +``` + +A job named `pdf` calls the `xelatex` command in order to build a pdf file from +the latex source file `mycv.tex`. We then define the `artifacts` paths which in +turn are defined with the `paths` keyword. All paths to files and directories +are relative to the repository that was cloned during the build. + +For more examples on artifacts, follow the +[separate artifacts yaml documentation](../yaml/README.md#artifacts). + +## Storing build artifacts + +After a successful build, GitLab Runner uploads an archive containing the build +artifacts to GitLab. + +To change the location where the artifacts are stored, follow the steps below. + +--- + +**In Omnibus installations:** + +_The artifacts are stored by default in +`/var/opt/gitlab/gitlab-rails/shared/artifacts`._ + +1. To change the storage path for example to `/mnt/storage/artifacts`, edit + `/etc/gitlab/gitlab.rb` and add the following line: + + ```ruby + gitlab_rails['artifacts_path'] = "/mnt/storage/artifacts" + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +--- + +**In installations from source:** + +_The artifacts are stored by default in +`/home/git/gitlab/shared/artifacts`._ + +1. To change the storage path for example to `/mnt/storage/artifacts`, edit + `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines: + + ```yaml + artifacts: + enabled: true + path: /mnt/storage/artifacts + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. + +## Browsing build artifacts + +When GitLab receives an artifacts archive, an archive metadata file is also +generated. This metadata file describes all the entries that are located in the +artifacts archive itself. The metadata file is in a binary format, with +additional GZIP compression. + +GitLab does not extract the artifacts archive in order to save space, memory +and disk I/O. It instead inspects the metadata file which contains all the +relevant information. This is especially important when there is a lot of +artifacts, or an archive is a very large file. + +--- + +After a successful build, if you visit the build's specific page, you can see +that there are two buttons. + +One is for downloading the artifacts archive and the other for browsing its +contents. + +![Build artifacts browser button](img/build_artifacts_browser_button.png) + +--- + +The archive browser shows the name and the actual file size of each file in the +archive. If your artifacts contained directories, then you are also able to +browse inside them. + +Below you can see an image of three different file formats, as well as two +directories. + +![Build artifacts browser](img/build_artifacts_browser.png) + +--- + +## Downloading build artifacts + +If you need to download the whole archive, there are buttons in various places +inside GitLab that make that possible. + +1. While on the builds page, you can see the download icon for each build's + artifacts archive in the right corner + +1. While inside a specific build, you are presented with a download button + along with the one that browses the archive + +1. And finally, when browsing and archive you can see the download button at + the top right corner + +--- + +Note that GitLab does not extract the entire artifacts archive to send just a +single file to the user. + +When clicking on a specific file, [GitLab Workhorse] extracts it from the +archive and the download begins. + +This implementation saves space, memory and disk I/O. + +[gitlab runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner "GitLab Runner repository" +[reconfigure gitlab]: ../../administration/restart_gitlab.md "How to restart GitLab documentation" +[restart gitlab]: ../../administration/restart_gitlab.md "How to restart GitLab documentation" +[gitlab workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse "GitLab Workhorse repository" diff --git a/doc/ci/build_artifacts/img/build_artifacts_browser.png b/doc/ci/build_artifacts/img/build_artifacts_browser.png new file mode 100644 index 00000000000..73ed4eeb927 Binary files /dev/null and b/doc/ci/build_artifacts/img/build_artifacts_browser.png differ diff --git a/doc/ci/build_artifacts/img/build_artifacts_browser_button.png b/doc/ci/build_artifacts/img/build_artifacts_browser_button.png new file mode 100644 index 00000000000..f5d15bc3e7d Binary files /dev/null and b/doc/ci/build_artifacts/img/build_artifacts_browser_button.png differ -- cgit v1.2.1 From 0689663487c66d21aa83da0830bc2c042f89e6e8 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 21 Jan 2016 18:19:18 +0100 Subject: Use branch_count in Repository#has_visible_content? Gitlab::Git::Repository#branch_count is a tad faster than the previous setup. See gitlab-org/gitlab_git!62 for more information. --- app/models/repository.rb | 2 +- spec/models/repository_spec.rb | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index d9ff71c01ed..f89a3890a49 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -57,7 +57,7 @@ class Repository # This method return true if repository contains some content visible in project page. # def has_visible_content? - !raw_repository.branches.empty? + raw_repository.branch_count > 0 end def commit(id = 'HEAD') diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index afbf62035ac..c484ae8fc8c 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -219,4 +219,24 @@ describe Repository, models: true do end end end + + describe '#has_visible_content?' do + subject { repository.has_visible_content? } + + describe 'when there are no branches' do + before do + allow(repository.raw_repository).to receive(:branch_count).and_return(0) + end + + it { is_expected.to eq(false) } + end + + describe 'when there are branches' do + before do + allow(repository.raw_repository).to receive(:branch_count).and_return(3) + end + + it { is_expected.to eq(true) } + end + end end -- cgit v1.2.1 From ba07a4c96a90a0cbe81ae7e987f32cc8eac20323 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 21 Jan 2016 18:28:09 +0100 Subject: update gitlab ci docker image version --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ac8390074f4..9bd558c7d0e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,5 @@ # This file is generated by GitLab CI +image: ruby:2.2 before_script: - ./scripts/prepare_build.sh - ruby -v -- cgit v1.2.1 From 8e14cb4b5ad1906b4f7337c85d9459581965c274 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 21 Jan 2016 12:51:24 -0500 Subject: Bump octokit to `~> 3.8.0` Closes #2877 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 746d8e5d6de..07ddbf87130 100644 --- a/Gemfile +++ b/Gemfile @@ -301,7 +301,7 @@ end gem "newrelic_rpm", '~> 3.9.4.245' gem 'newrelic-grape' -gem 'octokit', '~> 3.7.0' +gem 'octokit', '~> 3.8.0' gem "mail_room", "~> 0.6.1" diff --git a/Gemfile.lock b/Gemfile.lock index 5e0718af70f..cd11d388bbd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -492,7 +492,7 @@ GEM multi_json (~> 1.3) multi_xml (~> 0.5) rack (~> 1.2) - octokit (3.7.1) + octokit (3.8.0) sawyer (~> 0.6.0, >= 0.5.3) omniauth (1.2.2) hashie (>= 1.2, < 4) @@ -965,7 +965,7 @@ DEPENDENCIES nokogiri (= 1.6.7.1) nprogress-rails (~> 0.1.6.7) oauth2 (~> 1.0.0) - octokit (~> 3.7.0) + octokit (~> 3.8.0) omniauth (~> 1.2.2) omniauth-azure-oauth2 (~> 0.0.6) omniauth-bitbucket (~> 0.0.2) -- cgit v1.2.1 From 0f79620ccb049dc8146ab3c639f993122329eb83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Thu, 21 Jan 2016 12:54:10 -0500 Subject: Little refactor plus some specs. --- app/controllers/application_controller.rb | 18 ++++++------------ features/dashboard/dashboard.feature | 16 ++++++++++++++++ features/project/issues/issues.feature | 8 ++++++++ features/project/merge_requests.feature | 8 ++++++++ features/steps/dashboard/dashboard.rb | 1 + features/steps/shared/issuable.rb | 13 +++++++++++++ 6 files changed, 52 insertions(+), 12 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4c4e2ac3d3c..9df37a677c0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -410,18 +410,12 @@ class ApplicationController < ActionController::Base def set_default_sort controller_name = params[:controller].sub(/Controller\Z/, '').underscore - cookie_suffix = "_sort_#{controller_name}" - - key = if @project - "#{@project.cookie_key}#{cookie_suffix}" - elsif @group - "#{@group.cookie_key}#{cookie_suffix}" - else - "#{current_user.cookie_key}#{cookie_suffix}" - end - - cookies[key] ||= 'id_desc' - cookies[key] = params[:sort] if params[:sort].present? + cookie_suffix = "_sort_#{controller_name}" + + key = "#{(@project || @group || current_user).cookie_key}#{cookie_suffix}" + + cookies[key] ||= 'id_desc' + cookies[key] = params[:sort] if params[:sort].present? params[:sort] = cookies[key] end end diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature index b667b587c5b..9060e9d0ad2 100644 --- a/features/dashboard/dashboard.feature +++ b/features/dashboard/dashboard.feature @@ -41,3 +41,19 @@ Feature: Dashboard And user with name "John Doe" left project "Shop" When I visit dashboard activity page Then I should see "John Doe left project Shop" event + + @javascript + Scenario: Sorting Issues + Given I visit dashboard issues page + And I sort the list by "Oldest updated" + And I visit dashboard activity page + And I visit dashboard issues page + Then The list should be sorted by "Oldest updated" + + @javascript + Scenario: Sorting Merge Requests + Given I visit dashboard merge requests page + And I sort the list by "Oldest updated" + And I visit dashboard activity page + And I visit dashboard merge requests page + Then The list should be sorted by "Oldest updated" diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index 1502b0952cd..c1ae10f89c9 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -59,6 +59,14 @@ Feature: Project Issues And I sort the list by "Last updated" Then I should see "Release 0.4" at the top + @javascript + Scenario: Visiting Issues after being sorted the list + Given I visit project "Shop" issues page + And I sort the list by "Oldest updated" + And I visit my project's home page + And I visit project "Shop" issues page + Then The list should be sorted by "Oldest updated" + @javascript Scenario: I search issue Given I fill in issue search with "Re" diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index f1629a26f10..55c17ad0c15 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -84,6 +84,14 @@ Feature: Project Merge Requests And I sort the list by "Last updated" Then I should see "Bug NS-04" at the top + @javascript + Scenario: Visiting Merge Requests after being sorted the list + Given I visit project "Shop" merge requests page + And I sort the list by "Oldest updated" + And I visit my project's home page + And I visit project "Shop" merge requests page + Then The list should be sorted by "Oldest updated" + @javascript Scenario: Visiting Merge Requests after commenting on diffs Given project "Shop" have "Bug NS-05" open merge request with diffs inside diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index 63f0ec2b6e8..5062e348844 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -2,6 +2,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps include SharedAuthentication include SharedPaths include SharedProject + include SharedIssuable step 'I should see "New Project" link' do expect(page).to have_link "New project" diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb index 4c5f7488efb..25c2b476f43 100644 --- a/features/steps/shared/issuable.rb +++ b/features/steps/shared/issuable.rb @@ -106,6 +106,19 @@ module SharedIssuable edit_issuable end + step 'I sort the list by "Oldest updated"' do + find('button.dropdown-toggle.btn').click + page.within('ul.dropdown-menu.dropdown-menu-align-right li') do + click_link "Oldest updated" + end + end + + step 'The list should be sorted by "Oldest updated"' do + page.within('div.dropdown.inline.prepend-left-10') do + expect(page.find('button.dropdown-toggle.btn')).to have_content('Oldest updated') + end + end + def create_issuable_for_project(project_name:, title:, type: :issue) project = Project.find_by(name: project_name) -- cgit v1.2.1 From dc5a20520a02daaa589ad7c7c0a2d4d8ea7d208a Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 21 Jan 2016 19:57:41 +0100 Subject: Fix typos, make things more clear [ci skip] --- doc/administration/restart_gitlab.md | 43 +++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/doc/administration/restart_gitlab.md b/doc/administration/restart_gitlab.md index bb4fe9f414e..483060395dd 100644 --- a/doc/administration/restart_gitlab.md +++ b/doc/administration/restart_gitlab.md @@ -12,13 +12,7 @@ If you want the TL;DR versions, jump to: ## Omnibus installations If you have used the [Omnibus packages][omnibus-dl] to install GitLab, then -you should already have `gitlab-ctl` in your `PATH`. To find out, run: - -```bash -which gitlab-ctl -``` - -The output should be: `/usr/bin/gitlab-ctl`. +you should already have `gitlab-ctl` in your `PATH`. `gitlab-ctl` interacts with the Omnibus packages and can be used to restart the GitLab Rails application (Unicorn) as well as the other components, like: @@ -29,11 +23,12 @@ GitLab Rails application (Unicorn) as well as the other components, like: - NGINX (if you are using the bundled one) - Redis (if you are using the bundled one) - [Mailroom][] -- Logrotate. +- Logrotate ### Omnibus GitLab restart -When you are asked to _restart GitLab_, you need to run the following command: +There may be times in the documentation where you will be asked to _restart_ +GitLab. In that case, you need to run the following command: ```bash sudo gitlab-ctl restart @@ -65,14 +60,20 @@ To check the status of GitLab services, run: sudo gitlab-ctl status ``` -Notice that all services say `ok: run`. If you get any weird results, like the -unicorn service not starting, you may need to +Notice that all services say `ok: run`. + +Sometimes, components time out during the restart and sometimes they get stuck. +In that case, you can use `gitlab-ctl kill ` to send the `SIGKILL` +signal to the service, for example `sidekiq`. After that, a restart should +perform fine. + +As a last resort, you can try to [reconfigure GitLab](#omnibus-gitlab-reconfigure) instead. ### Omnibus GitLab reconfigure -There may be times where you will be asked to _reconfigure_ GitLab. Remember -that this method applies only for the Omnibus packages. +There may be times in the documentation where you will be asked to _reconfigure_ +GitLab. Remember that this method applies only for the Omnibus packages. Reconfigure Omnibus GitLab with: @@ -86,8 +87,14 @@ configuration (`/etc/gitlab/gitlab.rb`) has changed. When you run this command, [Chef], the underlying configuration management application that powers Omnibus GitLab, will make sure that all directories, permissions, services, etc., are in place and in the same shape that they were -initially shipped. This is where the _idempotency_ buzz-word you've been reading -here and there fits. +initially shipped. + +It will also restart GitLab components where needed, if any of their +configuration files have changed. + +If you manually edit any files in `/var/opt/gitlab` that are managed by Chef, +running reconfigure will revert the changes AND restart the services that +depend on those files. ## Installations from source @@ -120,14 +127,14 @@ GitLab and all its components are up and running. ``` This should restart Unicorn, Sidekiq, GitLab Workhorse and [Mailroom][] -(if enabled). The init service file that does all the magic can be found in -[`lib/support/init.d/gitlab`][src-service]. +(if enabled). The init service file that does all the magic can be found on +your server in `/etc/init.d/gitlab`. --- If you are using other init systems, like systemd, you can check the [GitLab Recipes][gl-recipes] repository for some unofficial services. These are -**not** officially supported so use at your own risk. +**not** officially supported so use them at your own risk. [omnibus-dl]: https://about.gitlab.com/downloads/ "Download the Omnibus packages" -- cgit v1.2.1 From eccaa099d185c351f9e8762661f0dc004f11f256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Thu, 21 Jan 2016 15:06:07 -0500 Subject: Make cookie suffix more simpler. --- app/controllers/application_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9df37a677c0..fb5900054ce 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -409,8 +409,8 @@ class ApplicationController < ActionController::Base private def set_default_sort - controller_name = params[:controller].sub(/Controller\Z/, '').underscore - cookie_suffix = "_sort_#{controller_name}" + # Use the controller name so we have a distinct cookie for Issues, MRs and Dashboard + cookie_suffix = "_sort_#{params[:controller].underscore}" key = "#{(@project || @group || current_user).cookie_key}#{cookie_suffix}" -- cgit v1.2.1 From 46c7691633cf45d2bae4e4d18fb978634f13623e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Thu, 21 Jan 2016 16:09:44 -0500 Subject: Add link for checkout MRs info. --- app/views/projects/merge_requests/show/_how_to_merge.html.haml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml index 877cc3d744b..0dbd159298e 100644 --- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml @@ -45,6 +45,10 @@ - unless @merge_request.can_be_merged_by?(current_user) %p Note that pushing to GitLab requires write access to this repository. + %p + %strong Tip: + You can also checkout merge requests locally by + %a{href: 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/workflow/merge_requests.md#checkout-merge-requests-locally', target: '_blank'} following these guidelines :javascript $(function(){ -- cgit v1.2.1 From 465e4616c980367a7e075ef7f795879810d9b1e2 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 21 Jan 2016 22:32:55 +0100 Subject: Add file finder documentation [ci skip] --- doc/workflow/README.md | 1 + doc/workflow/file_finder.md | 42 +++++++++++++++++++++++++++ doc/workflow/img/file_finder_find_button.png | Bin 0 -> 30974 bytes doc/workflow/img/file_finder_find_file.png | Bin 0 -> 42658 bytes 4 files changed, 43 insertions(+) create mode 100644 doc/workflow/file_finder.md create mode 100644 doc/workflow/img/file_finder_find_button.png create mode 100644 doc/workflow/img/file_finder_find_file.png diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 3651b55f438..bf62ab41053 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -6,6 +6,7 @@ - [GitLab Flow](gitlab_flow.md) - [Groups](groups.md) - [Keyboard shortcuts](shortcuts.md) +- [File finder](file_finder.md) - [Labels](labels.md) - [Notification emails](notifications.md) - [Project Features](project_features.md) diff --git a/doc/workflow/file_finder.md b/doc/workflow/file_finder.md new file mode 100644 index 00000000000..52ac5f032a5 --- /dev/null +++ b/doc/workflow/file_finder.md @@ -0,0 +1,42 @@ +# File finder + +_**Note:** This feature was [introduced][gh-9889] in GitLab 8.4._ + +--- + +The file finder feature allows you to quickly shortcut your way when you are +searching for a file in a repository using the GitLab UI. + +You can find the **Find File** button when in the **Files** section of a +project. + +![Find file button](img/file_finder_find_button.png) + +--- + +For the more lazy, there is a [shortcut button](shortcuts.md) as well. + +Go the **Files** section of a project and press `t` on your keyboard to launch +the search function. Start typing what you are searching for and watch the +magic being unfold. With the up/down arrows, you go up and down the results, +with `Esc` you close the search and go back to **Files**. + +## How it works + +The File finder feature is powered by the [Fuzzy filter] library. + +It implements a fuzzy search with highlight, and tries to provide intuitive +results by recognizing patterns that people use while searching. + +For example, consider the [GitLab CE repository][ce] and that we want to open +the `app/controllers/admin/deploy_keys_controller.rb` file. + +Using fuzzy search, we start by typing letters that get us closer to the file. + +**Protip:** To narrow down your search, include `/` in your search terms. + +![Find file button](img/file_finder_find_file.png) + +[gh-9889]: https://github.com/gitlabhq/gitlabhq/pull/9889 "File finder pull request" +[fuzzy filter]: https://github.com/jeancroy/fuzzaldrin-plus "fuzzaldrin-plus on GitHub" +[ce]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master "GitLab CE repository" diff --git a/doc/workflow/img/file_finder_find_button.png b/doc/workflow/img/file_finder_find_button.png new file mode 100644 index 00000000000..c5005d0d7ca Binary files /dev/null and b/doc/workflow/img/file_finder_find_button.png differ diff --git a/doc/workflow/img/file_finder_find_file.png b/doc/workflow/img/file_finder_find_file.png new file mode 100644 index 00000000000..58500f4c163 Binary files /dev/null and b/doc/workflow/img/file_finder_find_file.png differ -- cgit v1.2.1 From 9369b7ec1f98f8e94a0483e4fbb8c36c2333d60b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 21 Jan 2016 22:46:49 +0100 Subject: Fix MR diff 'Edit' button --- app/controllers/concerns/creates_commit.rb | 2 +- app/helpers/blob_helper.rb | 12 ++++----- app/helpers/commits_helper.rb | 2 +- app/views/projects/diffs/_file.html.haml | 40 ++++++++++++++---------------- 4 files changed, 27 insertions(+), 29 deletions(-) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 62127a09081..b9eb0a22f88 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -97,7 +97,7 @@ module CreatesCommit # Merge request from fork to this project @mr_source_project = @tree_edit_project @mr_target_project = @project - @mr_target_branch = @mr_target_project.repository.root_ref + @mr_target_branch = @ref end end end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 7c55934edda..8b689b29a41 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -26,10 +26,10 @@ module BlobHelper tree_join(ref, path), link_opts) - if !on_top_of_branch? + if !on_top_of_branch?(project, ref) button_tag "Edit", class: "btn btn-default disabled has_tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' } - elsif can_edit_blob?(blob) - link_to "Edit", edit_path, class: 'btn btn-small' + elsif can_edit_blob?(blob, project, ref) + link_to "Edit", edit_path, class: 'btn' elsif can?(current_user, :fork_project, project) continue_params = { to: edit_path, @@ -39,7 +39,7 @@ module BlobHelper fork_path = namespace_project_fork_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) - link_to "Edit", fork_path, class: 'btn btn-small', method: :post + link_to "Edit", fork_path, class: 'btn', method: :post end end @@ -50,11 +50,11 @@ module BlobHelper return unless blob - if !on_top_of_branch? + if !on_top_of_branch?(project, ref) button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' } elsif blob.lfs_pointer? button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' } - elsif can_edit_blob?(blob) + elsif can_edit_blob?(blob, project, ref) button_tag label, class: "btn btn-#{btn_class}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal' elsif can?(current_user, :fork_project, project) continue_params = { diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 590d20ac7b3..d26f007c8e6 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -166,7 +166,7 @@ module CommitsHelper link_to( namespace_project_blob_path(project.namespace, project, tree_join(commit_sha, diff.new_path)), - class: 'btn btn-small view-file js-view-file' + class: 'btn view-file js-view-file' ) do raw('View file @') + content_tag(:span, commit_sha[0..6], class: 'commit-short-id') diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 517f6aef7c5..fc0eaef2286 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -1,39 +1,37 @@ -.diff-file{id: "diff-#{i}", data: diff_file_html_data(project, diff_commit, diff_file)} - .diff-header{id: "file-path-#{hexdigest(diff_file.file_path)}"} +.diff-file.file-holder{id: "diff-#{i}", data: diff_file_html_data(project, diff_commit, diff_file)} + .file-title{id: "file-path-#{hexdigest(diff_file.file_path)}"} - if diff_file.diff.submodule? %span = icon('archive fw') %strong = submodule_link(blob, @commit.id, project.repository) - else - %span - = blob_icon blob.mode, blob.name - = link_to "#diff-#{i}" do - %strong - = diff_file.new_path + = blob_icon blob.mode, blob.name + = link_to "#diff-#{i}" do + %strong + = diff_file.new_path - - if diff_file.deleted_file - deleted - - elsif diff_file.renamed_file - renamed from - %strong - = diff_file.old_path + - if diff_file.deleted_file + deleted + - elsif diff_file.renamed_file + renamed from + %strong + = diff_file.old_path - - if diff_file.mode_changed? - %small - = "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}" + - if diff_file.mode_changed? + %small + = "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}" - .diff-controls + .file-actions.hidden-xs - if blob_text_viewable?(blob) - = link_to '#', class: 'js-toggle-diff-comments btn btn-sm active has_tooltip', title: "Toggle comments for this file" do - %i.fa.fa-comments -   + = link_to '#', class: 'js-toggle-diff-comments btn active has_tooltip', title: "Toggle comments for this file" do + = icon('comments') + \ - if editable_diff?(diff_file) = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, from_merge_request_id: @merge_request.id) -   = view_file_btn(diff_commit.id, diff_file, project) -- cgit v1.2.1 From e08e90edbbd28533f215f0dcf274baefb9fc87fd Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 21 Jan 2016 23:10:47 +0100 Subject: Refactor housekeeping documentation [ci skip] --- doc/administration/housekeeping.md | 21 +++++++++++++++------ doc/administration/img/housekeeping_settings.png | Bin 0 -> 23856 bytes 2 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 doc/administration/img/housekeeping_settings.png diff --git a/doc/administration/housekeeping.md b/doc/administration/housekeeping.md index b90f916fea9..c27cf1812dc 100644 --- a/doc/administration/housekeeping.md +++ b/doc/administration/housekeeping.md @@ -1,13 +1,22 @@ # Housekeeping -## Introduction +_**Note:** This feature was [introduced][ce-2371] in GitLab 8.4_ -The housekeeping function runs `git gc` on the current project git repository. +--- -`git gc` runs a number of housekeeping tasks, such as compressing file revisions (to reduce disk space and increase performance) and removing unreachable objects which may have been created from prior invocations of git add. +The housekeeping function runs [`git gc`][man] on the current project Git +repository. -See https://www.kernel.org/pub/software/scm/git/docs/git-gc.html for details. +`git gc` runs a number of housekeeping tasks, such as compressing file +revisions (to reduce disk space and increase performance) and removing +unreachable objects which may have been created from prior invocations of +`git add`. -## Where can I find it? +You can find this option under your **[Project] > Settings**. -Just go to your project settings page and you will find the housekeeping function below the project settings form. +--- + +![Housekeeping settings](img/housekeeping_settings.png) + +[ce-2371]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2371 "Housekeeping merge request" +[man]: https://www.kernel.org/pub/software/scm/git/docs/git-gc.html "git gc man page" diff --git a/doc/administration/img/housekeeping_settings.png b/doc/administration/img/housekeeping_settings.png new file mode 100644 index 00000000000..f7c5bc44367 Binary files /dev/null and b/doc/administration/img/housekeeping_settings.png differ -- cgit v1.2.1 From 68ac1a665846676057d4a845a1af3274db70c160 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 22 Jan 2016 00:23:20 +0100 Subject: File finder can be invoked from anywhere! [ci skip] --- doc/workflow/file_finder.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/doc/workflow/file_finder.md b/doc/workflow/file_finder.md index 52ac5f032a5..4bf0a179411 100644 --- a/doc/workflow/file_finder.md +++ b/doc/workflow/file_finder.md @@ -14,12 +14,15 @@ project. --- -For the more lazy, there is a [shortcut button](shortcuts.md) as well. +For the more lazy, there is a [shortcut button](shortcuts.md) as well, which +you can invoke from _anywhere_ in a project. -Go the **Files** section of a project and press `t` on your keyboard to launch -the search function. Start typing what you are searching for and watch the -magic being unfold. With the up/down arrows, you go up and down the results, -with `Esc` you close the search and go back to **Files**. +Press `t` on your keyboard to launch the File search function when in **Issues**, +**Merge requests**, **Milestones**, even the project's settings. + +Start typing what you are searching for and watch the magic being unfold. With +the up/down arrows, you go up and down the results, with `Esc` you close the +search and go back to **Files**. ## How it works -- cgit v1.2.1 From 150b4f66e6a244b5f6bd1119de3ce68336a9bf9e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 22 Jan 2016 01:43:06 +0100 Subject: Make sure non-highlighted diffs are still escaped --- app/controllers/projects/blob_controller.rb | 4 +++- app/models/note.rb | 8 ++++++-- lib/gitlab/diff/highlight.rb | 16 +++++++++++----- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 8133de90a41..bb72232edd7 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -52,7 +52,9 @@ class Projects::BlobController < Projects::ApplicationController def preview @content = params[:content] diffy = Diffy::Diff.new(@blob.data, @content, diff: '-U 3', include_diff_info: true) - @diff_lines = Gitlab::Diff::Parser.new.parse(diffy.diff.scan(/.*\n/)) + diff_lines = diffy.diff.scan(/.*\n/)[2..-1] + diff_lines = Gitlab::Diff::Parser.new.parse(diff_lines) + @diff_lines = Gitlab::Diff::Highlight.new(diff_lines).highlight render layout: false end diff --git a/app/models/note.rb b/app/models/note.rb index 15f48110ad2..55255d22c2f 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -244,7 +244,7 @@ class Note < ActiveRecord::Base prev_match_line = nil prev_lines = [] - diff_lines.each do |line| + highlighted_diff_lines.each do |line| if line.type == "match" prev_lines.clear prev_match_line = line @@ -261,7 +261,11 @@ class Note < ActiveRecord::Base end def diff_lines - @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.lines.to_a) + @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.lines) + end + + def highlighted_diff_lines + Gitlab::Diff::Highlight.new(diff_lines).highlight end def discussion_id diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index 179f8164c84..964c89de6c6 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -1,13 +1,17 @@ module Gitlab module Diff class Highlight - attr_reader :diff_file + attr_reader :diff_file, :diff_lines, :raw_lines delegate :old_path, :new_path, :old_ref, :new_ref, to: :diff_file, prefix: :diff - def initialize(diff_file) - @diff_file = diff_file - @diff_lines = diff_file.diff_lines + def initialize(diff_lines) + if diff_lines.is_a?(Gitlab::Diff::File) + @diff_file = diff_file + @diff_lines = diff_file.diff_lines + else + @diff_lines = diff_lines + end @raw_lines = @diff_lines.map(&:text) end @@ -31,7 +35,7 @@ module Gitlab private def highlight_line(diff_line, index) - return html_escape(diff_line.text) unless diff_file.diff_refs + return html_escape(diff_line.text) unless diff_file && diff_file.diff_refs line_prefix = diff_line.text.match(/\A(.)/) ? $1 : ' ' @@ -52,10 +56,12 @@ module Gitlab end def old_lines + return unless diff_file @old_lines ||= Gitlab::Highlight.highlight_lines(*processing_args(:old)) end def new_lines + return unless diff_file @new_lines ||= Gitlab::Highlight.highlight_lines(*processing_args(:new)) end -- cgit v1.2.1 From 82d0fa8e451de384150394d8465f6f165adfeae9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 22 Jan 2016 01:53:01 +0100 Subject: Fix --- lib/gitlab/diff/highlight.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index 964c89de6c6..9283b5e185d 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -7,8 +7,8 @@ module Gitlab def initialize(diff_lines) if diff_lines.is_a?(Gitlab::Diff::File) - @diff_file = diff_file - @diff_lines = diff_file.diff_lines + @diff_file = diff_lines + @diff_lines = @diff_file.diff_lines else @diff_lines = diff_lines end -- cgit v1.2.1 From 16d17b78cabe2efd762a4aa0e9fa2c2792fab32d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 22 Jan 2016 02:10:58 +0100 Subject: Restore diff comments --- app/views/projects/diffs/_text_file.html.haml | 3 ++- lib/gitlab/diff/highlight.rb | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index f4fc6caba0f..2e329a823a6 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -6,6 +6,7 @@ %table.text-file.code.js-syntax-highlight{ class: too_big ? 'hide' : '' } - last_line = 0 + - raw_diff_lines = diff_file.diff_lines - diff_file.highlighted_diff_lines.each_with_index do |line, index| - type = line.type - last_line = line.new_pos @@ -31,7 +32,7 @@ - if @reply_allowed - comments = @line_notes.select { |n| n.line_code == line_code && n.active? }.sort_by(&:created_at) - unless comments.empty? - = render "projects/notes/diff_notes_with_reply", notes: comments, line: line.text + = render "projects/notes/diff_notes_with_reply", notes: comments, line: raw_diff_lines[index].text - if last_line > 0 = render "projects/diffs/match_line", {line: "", diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index 179f8164c84..ac8537ad31a 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -12,9 +12,10 @@ module Gitlab end def highlight - @diff_lines.each_with_index do |diff_line, i| + @diff_lines.map.with_index do |diff_line, i| + diff_line = diff_line.dup # ignore highlighting for "match" lines - next if diff_line.type == 'match' || diff_line.type == 'nonewline' + next diff_line if diff_line.type == 'match' || diff_line.type == 'nonewline' rich_line = highlight_line(diff_line, i) @@ -23,9 +24,9 @@ module Gitlab end diff_line.text = rich_line.html_safe - end - @diff_lines + diff_line + end end private -- cgit v1.2.1 From 3db24ec9e80369834ccf7d2423808fcf5e51b5a6 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 22 Jan 2016 02:12:43 +0100 Subject: Properly highlight right side of parallel diff --- lib/gitlab/diff/parallel_diff.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/diff/parallel_diff.rb b/lib/gitlab/diff/parallel_diff.rb index c0db3559e3a..74f9b3c050a 100644 --- a/lib/gitlab/diff/parallel_diff.rb +++ b/lib/gitlab/diff/parallel_diff.rb @@ -11,7 +11,8 @@ module Gitlab lines = [] skip_next = false - diff_file.highlighted_diff_lines.each do |line| + highlighted_diff_lines = diff_file.highlighted_diff_lines + highlighted_diff_lines.each do |line| full_line = line.text type = line.type line_code = generate_line_code(diff_file.file_path, line) @@ -21,6 +22,7 @@ module Gitlab next_line = diff_file.next_line(line.index) if next_line + next_line = highlighted_diff_lines[next_line.index] next_line_code = generate_line_code(diff_file.file_path, next_line) next_type = next_line.type next_line = next_line.text -- cgit v1.2.1 From de7ee6c37bbf26509c1c533d915f6dbaecdd6ab3 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 22 Jan 2016 08:26:48 +0100 Subject: Fix grammar and typos in file finder docs [ci skip] --- doc/workflow/file_finder.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/doc/workflow/file_finder.md b/doc/workflow/file_finder.md index 4bf0a179411..b69ae663272 100644 --- a/doc/workflow/file_finder.md +++ b/doc/workflow/file_finder.md @@ -14,15 +14,16 @@ project. --- -For the more lazy, there is a [shortcut button](shortcuts.md) as well, which -you can invoke from _anywhere_ in a project. +For those who prefer to keep their fingers on the keyboard, there is a +[shortcut button](shortcuts.md) as well, which you can invoke from _anywhere_ +in a project. -Press `t` on your keyboard to launch the File search function when in **Issues**, +Press `t` to launch the File search function when in **Issues**, **Merge requests**, **Milestones**, even the project's settings. -Start typing what you are searching for and watch the magic being unfold. With -the up/down arrows, you go up and down the results, with `Esc` you close the -search and go back to **Files**. +Start typing what you are searching for and watch the magic happen. With the +up/down arrows, you go up and down the results, with `Esc` you close the search +and go back to **Files**. ## How it works -- cgit v1.2.1 From 373b277f033e12fd9a3a1e12367dd5cf120f3586 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 22 Jan 2016 10:08:57 +0100 Subject: Add note about commit status API deprecation [ci skip] --- doc/api/commits.md | 9 +++++++-- doc/ci/api/commits.md | 7 +++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/doc/api/commits.md b/doc/api/commits.md index 4008629e0a2..e4d436b8e52 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -211,7 +211,12 @@ Example response: } ``` -## Get the status of a commit +## Commit status + +Since GitLab 8.1, this is the new commit status API. The documentation in +[ci/api/commits](../ci/api/commits.md) is deprecated. + +### Get the status of a commit Get the statuses of a commit in a project. @@ -285,7 +290,7 @@ Example response: ] ``` -## Post the build status to a commit +### Post the build status to a commit Adds or updates a build status of a commit. diff --git a/doc/ci/api/commits.md b/doc/ci/api/commits.md index 4df7afc6c52..871de7abcce 100644 --- a/doc/ci/api/commits.md +++ b/doc/ci/api/commits.md @@ -1,5 +1,12 @@ # Commits API +**DEPRECATED** + +Since GitLab 8.1, there is a new commit status API. Please see the [revised +documentation](../../api/commits.md#commit-status). + +--- + __Authentication is done by GitLab CI project token__ ## Commits -- cgit v1.2.1 From dba2e9c31807725dfd3e40bd055f11a15cf68d32 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 22 Jan 2016 10:23:32 +0100 Subject: Add image to 2fa security documentation [ci skip] --- doc/security/img/two_factor_authentication_settings.png | Bin 0 -> 20399 bytes doc/security/two_factor_authentication.md | 15 +++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 doc/security/img/two_factor_authentication_settings.png diff --git a/doc/security/img/two_factor_authentication_settings.png b/doc/security/img/two_factor_authentication_settings.png new file mode 100644 index 00000000000..aa51ce030bb Binary files /dev/null and b/doc/security/img/two_factor_authentication_settings.png differ diff --git a/doc/security/two_factor_authentication.md b/doc/security/two_factor_authentication.md index 4e25a1fdc3f..8365bdb7b1b 100644 --- a/doc/security/two_factor_authentication.md +++ b/doc/security/two_factor_authentication.md @@ -20,7 +20,13 @@ In the Admin area under **Settings** (`/admin/application_settings`), look for the "Sign-in Restrictions" area, where you can configure both. If you want 2FA enforcement to take effect on next login, change the grace -period to `0` +period to `0`. + +--- + +![Two factor authentication admin settings](img/two_factor_authentication_settings.png) + +--- ## Disabling 2FA for everyone @@ -28,11 +34,12 @@ There may be some special situations where you want to disable 2FA for everyone even when forced 2FA is disabled. There is a rake task for that: ``` -# use this command if you've installed GitLab with the Omnibus package +# Omnibus installations sudo gitlab-rake gitlab:two_factor:disable_for_all_users -# if you've installed GitLab from source +# Installations from source sudo -u git -H bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production ``` -**IMPORTANT: this is a permanent and irreversible action. Users will have to reactivate 2FA from scratch if they want to use it again.** +**IMPORTANT: this is a permanent and irreversible action. Users will have to + reactivate 2FA from scratch if they want to use it again.** -- cgit v1.2.1 From be7bc9d9b096a3d45bd8f58d1946bc80fea1c7f0 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 22 Jan 2016 11:24:38 +0200 Subject: Backport some changes from EE --- app/helpers/snippets_helper.rb | 75 +++++++++++++++++++++++++++++++++ app/models/issue.rb | 1 + app/models/project.rb | 4 ++ app/models/project_wiki.rb | 1 + lib/gitlab/snippet_search_results.rb | 82 +----------------------------------- 5 files changed, 83 insertions(+), 80 deletions(-) diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 906cb12cd48..bc36434f549 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -17,4 +17,79 @@ module SnippetsHelper snippet_path(snippet) end end + + # Get an array of line numbers surrounding a matching + # line, bounded by min/max. + # + # @returns Array of line numbers + def bounded_line_numbers(line, min, max, surrounding_lines) + lower = line - surrounding_lines > min ? line - surrounding_lines : min + upper = line + surrounding_lines < max ? line + surrounding_lines : max + (lower..upper).to_a + end + + # Returns a sorted set of lines to be included in a snippet preview. + # This ensures matching adjacent lines do not display duplicated + # surrounding code. + # + # @returns Array, unique and sorted. + def matching_lines(lined_content, surrounding_lines) + used_lines = [] + lined_content.each_with_index do |line, line_number| + used_lines.concat bounded_line_numbers( + line_number, + 0, + lined_content.size, + surrounding_lines + ) if line.include?(query) + end + + used_lines.uniq.sort + end + + # 'Chunkify' entire snippet. Splits the snippet data into matching lines + + # surrounding_lines() worth of unmatching lines. + # + # @returns a hash with {snippet_object, snippet_chunks:{data,start_line}} + def chunk_snippet(snippet, surrounding_lines = 3) + lined_content = snippet.content.split("\n") + used_lines = matching_lines(lined_content, surrounding_lines) + + snippet_chunk = [] + snippet_chunks = [] + snippet_start_line = 0 + last_line = -1 + + # Go through each used line, and add consecutive lines as a single chunk + # to the snippet chunk array. + used_lines.each do |line_number| + if last_line < 0 + # Start a new chunk. + snippet_start_line = line_number + snippet_chunk << lined_content[line_number] + elsif last_line == line_number - 1 + # Consecutive line, continue chunk. + snippet_chunk << lined_content[line_number] + else + # Non-consecutive line, add chunk to chunk array. + snippet_chunks << { + data: snippet_chunk.join("\n"), + start_line: snippet_start_line + 1 + } + + # Start a new chunk. + snippet_chunk = [lined_content[line_number]] + snippet_start_line = line_number + end + last_line = line_number + end + # Add final chunk to chunk array + snippet_chunks << { + data: snippet_chunk.join("\n"), + start_line: snippet_start_line + 1 + } + + # Return snippet with chunk array + { snippet_object: snippet, snippet_chunks: snippet_chunks } + end end diff --git a/app/models/issue.rb b/app/models/issue.rb index 7beba984608..5f58c0508fd 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -38,6 +38,7 @@ class Issue < ActiveRecord::Base scope :cared, ->(user) { where(assignee_id: user) } scope :open_for, ->(user) { opened.assigned_to(user) } + scope :in_projects, ->(project_ids) { where(project_id: project_ids) } state_machine :state, initial: :opened do event :close do diff --git a/app/models/project.rb b/app/models/project.rb index 5579710a476..4bd51449c25 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -904,4 +904,8 @@ class Project < ActiveRecord::Base def runners_token ensure_runners_token! end + + def wiki + @wiki ||= ProjectWiki.new(self, self.owner) + end end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 8ce47495971..c847eba8d1c 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -12,6 +12,7 @@ class ProjectWiki # Returns a string describing what went wrong after # an operation fails. attr_reader :error_message + attr_reader :project def initialize(project, user = nil) @project = project diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb index 938219efdb2..38364a0b151 100644 --- a/lib/gitlab/snippet_search_results.rb +++ b/lib/gitlab/snippet_search_results.rb @@ -1,5 +1,7 @@ module Gitlab class SnippetSearchResults < SearchResults + include SnippetsHelper + attr_reader :limit_snippet_ids def initialize(limit_snippet_ids, query) @@ -47,85 +49,5 @@ module Gitlab def default_scope 'snippet_blobs' end - - # Get an array of line numbers surrounding a matching - # line, bounded by min/max. - # - # @returns Array of line numbers - def bounded_line_numbers(line, min, max) - lower = line - surrounding_lines > min ? line - surrounding_lines : min - upper = line + surrounding_lines < max ? line + surrounding_lines : max - (lower..upper).to_a - end - - # Returns a sorted set of lines to be included in a snippet preview. - # This ensures matching adjacent lines do not display duplicated - # surrounding code. - # - # @returns Array, unique and sorted. - def matching_lines(lined_content) - used_lines = [] - lined_content.each_with_index do |line, line_number| - used_lines.concat bounded_line_numbers( - line_number, - 0, - lined_content.size - ) if line.include?(query) - end - - used_lines.uniq.sort - end - - # 'Chunkify' entire snippet. Splits the snippet data into matching lines + - # surrounding_lines() worth of unmatching lines. - # - # @returns a hash with {snippet_object, snippet_chunks:{data,start_line}} - def chunk_snippet(snippet) - lined_content = snippet.content.split("\n") - used_lines = matching_lines(lined_content) - - snippet_chunk = [] - snippet_chunks = [] - snippet_start_line = 0 - last_line = -1 - - # Go through each used line, and add consecutive lines as a single chunk - # to the snippet chunk array. - used_lines.each do |line_number| - if last_line < 0 - # Start a new chunk. - snippet_start_line = line_number - snippet_chunk << lined_content[line_number] - elsif last_line == line_number - 1 - # Consecutive line, continue chunk. - snippet_chunk << lined_content[line_number] - else - # Non-consecutive line, add chunk to chunk array. - snippet_chunks << { - data: snippet_chunk.join("\n"), - start_line: snippet_start_line + 1 - } - - # Start a new chunk. - snippet_chunk = [lined_content[line_number]] - snippet_start_line = line_number - end - last_line = line_number - end - # Add final chunk to chunk array - snippet_chunks << { - data: snippet_chunk.join("\n"), - start_line: snippet_start_line + 1 - } - - # Return snippet with chunk array - { snippet_object: snippet, snippet_chunks: snippet_chunks } - end - - # Defines how many unmatching lines should be - # included around the matching lines in a snippet - def surrounding_lines - 3 - end end end -- cgit v1.2.1 From 49233288eaaabaa7ab05a96fa25c2c411429fffa Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 22 Jan 2016 10:44:27 +0100 Subject: Fix wording and appearance on build artifacts documentation [ci skip] --- doc/ci/build_artifacts/README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/doc/ci/build_artifacts/README.md b/doc/ci/build_artifacts/README.md index b02caa9edff..61418cef2c6 100644 --- a/doc/ci/build_artifacts/README.md +++ b/doc/ci/build_artifacts/README.md @@ -13,13 +13,11 @@ ability of downloading the files separately. ## Enabling build artifacts -If you are searching for ways to use the artifacts feature, jump to -[Defining artifacts in `.gitlab-ci.yml`](#defining-artifacts-in-gitlab-ciyml). +_If you are searching for ways to use artifacts, jump to +[Defining artifacts in `.gitlab-ci.yml`](#defining-artifacts-in-gitlab-ciyml)._ The artifacts feature is enabled by default in all GitLab installations. - -If by any chance you want to disable the artifacts feature on your GitLab -instance, follow the steps below. +To disable it site-wide, follow the steps below. --- -- cgit v1.2.1 From b75f6c68e591b1a6a279c454b7c5d1f6f8ecbbf8 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 22 Jan 2016 11:42:29 +0100 Subject: Add tests for clicking a row in build artifacts browser --- features/project/builds/artifacts.feature | 9 +++++++++ features/steps/project/builds/artifacts.rb | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/features/project/builds/artifacts.feature b/features/project/builds/artifacts.feature index 1185854453a..52dc15f2eb6 100644 --- a/features/project/builds/artifacts.feature +++ b/features/project/builds/artifacts.feature @@ -51,3 +51,12 @@ Feature: Project Builds Artifacts And I click artifacts browse button And I click a link to file within build artifacts Then download of a file extracted from build artifacts should start + + @javascript + Scenario: I click on a row in an artifacts table + Given recent build has artifacts available + And recent build has artifacts metadata available + When I visit recent build details page + And I click artifacts browse button + And I click a first row within build artifacts table + Then page with a coresponding path is loading diff --git a/features/steps/project/builds/artifacts.rb b/features/steps/project/builds/artifacts.rb index 25f2f4e837c..1bdb57af9d1 100644 --- a/features/steps/project/builds/artifacts.rb +++ b/features/steps/project/builds/artifacts.rb @@ -73,4 +73,14 @@ class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps expect(response_json[:archive]).to end_with('build_artifacts.zip') expect(response_json[:entry]).to eq Base64.encode64('ci_artifacts.txt') end + + step 'I click a first row within build artifacts table' do + row = first('tr[data-link]') + @row_path = row['data-link'] + row.click + end + + step 'page with a coresponding path is loading' do + expect(current_path).to eq @row_path + end end -- cgit v1.2.1 From 865a82ebca9aafe4f40f98612c2fe29957b2f18c Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 22 Jan 2016 11:53:59 +0100 Subject: Remove 8.4 unreleased note from changelog [ci skip] --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 6985c1fa46e..7c14a922c88 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,7 +5,7 @@ v 8.5.0 (unreleased) - Upgrade gitlab_git to 7.2.23 to fix commit message mentions in first branch push - New UI for pagination -v 8.4.0 (unreleased) +v 8.4.0 - Allow LDAP users to change their email if it was not set by the LDAP server - Ensure Gravatar host looks like an actual host - Consider re-assign as a mention from a notification point of view -- cgit v1.2.1 From 2254413267f69ddf2a84a37ae4c90b5cc248ec94 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 22 Jan 2016 12:33:37 +0100 Subject: Fix broken codeblocks in ci/yaml documentation [ci skip] --- doc/ci/yaml/README.md | 63 +++++++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 3b594df659d..c8d5ecb8eef 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -159,50 +159,55 @@ The `key` directive allows you to define the affinity of caching between jobs, allowing to have a single cache for all jobs, cache per-job, cache per-branch or any other way you deem proper. -This allows you to fine tune caching, allowing you to cache data between different jobs or even different branches. -The `cache:key` variable can use any of the [predefined variables](../variables/README.md): +This allows you to fine tune caching, allowing you to cache data between +different jobs or even different branches. -Example configurations: +The `cache:key` variable can use any of the [predefined variables](../variables/README.md). + +--- + +**Example configurations** To enable per-job caching: - ```yaml - cache: - key: "$CI_BUILD_NAME" - untracked: true - ``` +```yaml +cache: + key: "$CI_BUILD_NAME" + untracked: true +``` To enable per-branch caching: - ```yaml - cache: - key: "$CI_BUILD_REF_NAME" - untracked: true - ``` +```yaml +cache: + key: "$CI_BUILD_REF_NAME" + untracked: true +``` To enable per-job and per-branch caching: - ```yaml - cache: - key: "$CI_BUILD_NAME/$CI_BUILD_REF_NAME" - untracked: true - ``` +```yaml +cache: + key: "$CI_BUILD_NAME/$CI_BUILD_REF_NAME" + untracked: true +``` To enable per-branch and per-stage caching: - ```yaml - cache: - key: "$CI_BUILD_STAGE/$CI_BUILD_REF_NAME" - untracked: true - ``` +```yaml +cache: + key: "$CI_BUILD_STAGE/$CI_BUILD_REF_NAME" + untracked: true +``` -If you use **Windows Batch** to run your shell scripts you need to replace the `$` with `%`: +If you use **Windows Batch** to run your shell scripts you need to replace +`$` with `%`: - ```yaml - cache: - key: "%CI_BUILD_STAGE%/%CI_BUILD_REF_NAME%" - untracked: true - ``` +```yaml +cache: + key: "%CI_BUILD_STAGE%/%CI_BUILD_REF_NAME%" + untracked: true +``` ## Jobs -- cgit v1.2.1 From 3a5cca26c978656391c2304675e5f4aa090715a3 Mon Sep 17 00:00:00 2001 From: Julian Rademacher Date: Fri, 22 Jan 2016 11:36:11 +0000 Subject: GitLab 8.4 uses gitlab-shell v2.6.10 --- doc/update/8.3-to-8.4.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/update/8.3-to-8.4.md b/doc/update/8.3-to-8.4.md index bf80f66d004..164cbcb53ce 100644 --- a/doc/update/8.3-to-8.4.md +++ b/doc/update/8.3-to-8.4.md @@ -37,7 +37,7 @@ sudo -u git -H git checkout 8-4-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch --all -sudo -u git -H git checkout v2.6.9 +sudo -u git -H git checkout v2.6.10 ``` ### 5. Update gitlab-workhorse -- cgit v1.2.1 From 33dc2f30359968c80b14d43de6fad2bb07460bd1 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 22 Jan 2016 12:45:49 +0100 Subject: Make git gc man page more apparent [ci skip] --- doc/administration/housekeeping.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/administration/housekeeping.md b/doc/administration/housekeeping.md index c27cf1812dc..a5fa7d358a2 100644 --- a/doc/administration/housekeeping.md +++ b/doc/administration/housekeeping.md @@ -4,8 +4,8 @@ _**Note:** This feature was [introduced][ce-2371] in GitLab 8.4_ --- -The housekeeping function runs [`git gc`][man] on the current project Git -repository. +The housekeeping function runs `git gc` ([man page][man]) on the current +project Git repository. `git gc` runs a number of housekeeping tasks, such as compressing file revisions (to reduce disk space and increase performance) and removing -- cgit v1.2.1 From 6c4961ae4857065e0c2f49ffbc1c4206b80bcc78 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 22 Jan 2016 12:53:00 +0100 Subject: Allow empty Mysql password on CI --- .gitlab-ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ac8390074f4..cbc812a3b88 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,6 +10,9 @@ before_script: - bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" - RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate +variables: + MYSQL_ALLOW_EMPTY_PASSWORD: "1" + spec:feature: script: - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null -- cgit v1.2.1 From d91335293a2c950503f36071899300030b6ec0b1 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 22 Jan 2016 14:45:34 +0100 Subject: More docker stuff in gitlab-ci.yml --- .gitlab-ci.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cbc812a3b88..2dc09e4a1a5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,13 @@ -# This file is generated by GitLab CI +image: "ruby:2.1" + +services: + - mysql:latest + - postgres:latest + - redis:latest + +variables: + MYSQL_ALLOW_EMPTY_PASSWORD: "1" + before_script: - ./scripts/prepare_build.sh - ruby -v @@ -10,9 +19,6 @@ before_script: - bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" - RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate -variables: - MYSQL_ALLOW_EMPTY_PASSWORD: "1" - spec:feature: script: - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null -- cgit v1.2.1 From db7d1dbe54714627bf156f0a4133d34ae750f3b5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 22 Jan 2016 16:07:24 +0100 Subject: Make old line background color for Solarized Dark a little more clear --- app/assets/stylesheets/highlight/solarized_dark.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index ae7ecc65c39..b0aaadd0f7f 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -26,7 +26,7 @@ } .diff-line-num.old, .line_content.old { - @include diff_background(rgba(220, 50, 47, 0.2), rgba(220, 50, 47, 0.3), #808080); + @include diff_background(rgba(220, 50, 47, 0.3), rgba(220, 50, 47, 0.3), #808080); } .line_content.match { -- cgit v1.2.1 From 831bb04db7f4be7c7207c287339815177112f356 Mon Sep 17 00:00:00 2001 From: Mark Nordine Date: Fri, 22 Jan 2016 16:04:08 +0000 Subject: Minor spelling, prob typo --- doc/ci/build_artifacts/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/build_artifacts/README.md b/doc/ci/build_artifacts/README.md index 61418cef2c6..b112beef7b0 100644 --- a/doc/ci/build_artifacts/README.md +++ b/doc/ci/build_artifacts/README.md @@ -152,7 +152,7 @@ inside GitLab that make that possible. 1. While inside a specific build, you are presented with a download button along with the one that browses the archive -1. And finally, when browsing and archive you can see the download button at +1. And finally, when browsing an archive you can see the download button at the top right corner --- -- cgit v1.2.1 From 5972dedfcfda1826617092b018e68757c18d995a Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 22 Jan 2016 08:17:54 -0800 Subject: Ignore binary files in code search to prevent Error 500 Leaving out the -I option in "git grep" would cause an Error 500 because the resulting line would include "Binary file X matches" --- CHANGELOG | 1 + app/models/repository.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 7c14a922c88..0d3e0571b59 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.5.0 (unreleased) - Add "visibility" flag to GET /projects api endpoint + - Ignore binary files in code search to prevent Error 500 (Stan Hu) - Upgrade gitlab_git to 7.2.23 to fix commit message mentions in first branch push - New UI for pagination diff --git a/app/models/repository.rb b/app/models/repository.rb index d9ff71c01ed..9e8bd91fde9 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -598,7 +598,7 @@ class Repository def search_files(query, ref) offset = 2 - args = %W(#{Gitlab.config.git.bin_path} grep -i -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref}) + args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref}) Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/) end -- cgit v1.2.1 From fd00646b5b82b6a890c5cf0a825fa420abd29799 Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Fri, 22 Jan 2016 08:57:29 -0800 Subject: Name more OmniAuth providers in the general readme. --- doc/integration/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/integration/README.md b/doc/integration/README.md index 5edac746c7b..846526f4e80 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -8,7 +8,7 @@ See the documentation below for details on how to configure these services. - [Jira](jira.md) Integrate with the JIRA issue tracker - [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. - [LDAP](ldap.md) Set up sign in via LDAP -- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab, and Google via OAuth. +- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd and Azure - [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider - [CAS](cas.md) Configure GitLab to sign in using CAS - [Slack](slack.md) Integrate with the Slack chat service -- cgit v1.2.1 From edf93ecb6eab702de1a2ec24d58d1ad1dd1d9e55 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 22 Jan 2016 18:16:59 +0100 Subject: Speed-up docker based builds Enable to: - Use APT cache - Use Bundler cache - Use PhantomJS cache --- .gitlab-ci.yml | 2 +- scripts/ci/prepare_build.sh | 22 ---------------------- scripts/prepare_build.sh | 16 +++++++++++----- 3 files changed, 12 insertions(+), 28 deletions(-) delete mode 100755 scripts/ci/prepare_build.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2dc09e4a1a5..ec53271b6bc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,7 +9,7 @@ variables: MYSQL_ALLOW_EMPTY_PASSWORD: "1" before_script: - - ./scripts/prepare_build.sh + - source ./scripts/prepare_build.sh - ruby -v - which ruby - gem install bundler --no-ri --no-rdoc diff --git a/scripts/ci/prepare_build.sh b/scripts/ci/prepare_build.sh deleted file mode 100755 index 864a683a1bd..00000000000 --- a/scripts/ci/prepare_build.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -if [ -f /.dockerinit ]; then - export FLAGS=(--deployment --path /cache) - - apt-get update -qq - apt-get install -y -qq nodejs - - wget -q http://ftp.de.debian.org/debian/pool/main/p/phantomjs/phantomjs_1.9.0-1+b1_amd64.deb - dpkg -i phantomjs_1.9.0-1+b1_amd64.deb - - cp config/database.yml.mysql config/database.yml - sed -i "s/username:.*/username: root/g" config/database.yml - sed -i "s/password:.*/password:/g" config/database.yml - sed -i "s/# socket:.*/host: mysql/g" config/database.yml -else - export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin - - cp config/database.yml.mysql config/database.yml - sed -i "s/username\:.*$/username\: runner/" config/database.yml - sed -i "s/password\:.*$/password\: 'password'/" config/database.yml - sed -i "s/gitlab_ci_test/gitlab_ci_test_$((RANDOM/5000))/" config/database.yml -fi diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index 119cc90fc1e..fb648d27621 100755 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -1,10 +1,16 @@ #!/bin/bash + if [ -f /.dockerinit ]; then - wget -q https://gitlab.com/axil/phantomjs-debian/raw/master/phantomjs_1.9.8-0jessie_amd64.deb - dpkg -i phantomjs_1.9.8-0jessie_amd64.deb + # Docker runners use `/cache` folder which is persisted every build + if [ ! -e /cache/phantomjs_1.9.8-0jessie_amd64.deb ]; then + wget -q https://gitlab.com/axil/phantomjs-debian/raw/master/phantomjs_1.9.8-0jessie_amd64.deb + mv phantomjs_1.9.8-0jessie_amd64.deb /cache + fi + dpkg -i /cache/phantomjs_1.9.8-0jessie_amd64.deb apt-get update -qq - apt-get install -y -qq libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client + apt-get -o dir::cache::archives="/cache/apt" install -y -qq \ + libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client cp config/database.yml.mysql config/database.yml sed -i 's/username:.*/username: root/g' config/database.yml @@ -13,8 +19,8 @@ if [ -f /.dockerinit ]; then cp config/resque.yml.example config/resque.yml sed -i 's/localhost/redis/g' config/resque.yml - FLAGS=(--deployment --path /cache) - export FLAGS + + export FLAGS=(--path /cache) else export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin cp config/database.yml.mysql config/database.yml -- cgit v1.2.1 From bed4f43f1ad46a53223f86a444780efa138f126f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 22 Jan 2016 10:20:39 -0800 Subject: Fix changelog Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a8fee206890..0c4d90855a5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,10 +4,9 @@ v 8.5.0 (unreleased) - Add "visibility" flag to GET /projects api endpoint - Upgrade gitlab_git to 7.2.23 to fix commit message mentions in first branch push - New UI for pagination + - Fix diff comments loaded by AJAX to load comment with diff in discussion tab v 8.4.0 -v 8.4.0 (unreleased) - - Fix diff comments loaded by AJAX to load comment with diff in discussion tab - Allow LDAP users to change their email if it was not set by the LDAP server - Ensure Gravatar host looks like an actual host - Consider re-assign as a mention from a notification point of view -- cgit v1.2.1 From 892844c532fc2f03616ba6e7af7c458e93ff19e2 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 22 Jan 2016 13:04:22 -0800 Subject: Don't use production section for Gemfile Signed-off-by: Dmitriy Zaporozhets --- Gemfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 746d8e5d6de..8e727d950bf 100644 --- a/Gemfile +++ b/Gemfile @@ -212,6 +212,9 @@ gem 'select2-rails', '~> 3.5.9' gem 'virtus', '~> 1.0.1' gem 'net-ssh', '~> 3.0.1' +# Sentry integration +gem 'sentry-raven' + # Metrics group :metrics do gem 'allocations', '~> 1.0', require: false, platform: :mri @@ -293,9 +296,6 @@ end group :production do gem "gitlab_meta", '7.0' - - # Sentry integration - gem 'sentry-raven' end gem "newrelic_rpm", '~> 3.9.4.245' -- cgit v1.2.1 From cf673ba9f64b6c9c8b06e46cb3c541ab87924009 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 22 Jan 2016 13:37:05 -0800 Subject: Fix UI for pages with panels Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/panels.scss | 12 +++++++----- app/assets/stylesheets/framework/tw_bootstrap.scss | 13 ------------- app/views/admin/deploy_keys/index.html.haml | 2 +- app/views/admin/projects/index.html.haml | 4 ++-- app/views/groups/edit.html.haml | 1 - app/views/groups/group_members/index.html.haml | 3 +-- app/views/groups/projects.html.haml | 4 ++-- app/views/profiles/accounts/show.html.haml | 1 - app/views/projects/project_members/_group_members.html.haml | 2 +- app/views/projects/project_members/_team.html.haml | 2 +- app/views/projects/project_members/index.html.haml | 3 +-- 11 files changed, 16 insertions(+), 31 deletions(-) diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index 57b9451b264..ae7bdf14c40 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -2,7 +2,13 @@ margin-bottom: $gl-padding; .panel-heading { - padding: 7px $gl-padding; + padding: $gl-vert-padding $gl-padding; + line-height: 36px; + + .controls { + margin-top: -2px; + float: right; + } } .panel-body { @@ -14,7 +20,3 @@ } } } - -.container-blank .panel .panel-heading { - line-height: 42px !important; -} diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 88072606bf5..3e709244879 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -114,22 +114,9 @@ * */ -.container-blank .panel .panel-heading { - font-size: 17px; - line-height: 38px; -} - .panel { box-shadow: none; - .panel-heading { - .panel-head-actions { - position: relative; - top: -5px; - float: right; - } - } - .panel-body { form, pre { margin: 0; diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml index 841e6971fb2..41c43899978 100644 --- a/app/views/admin/deploy_keys/index.html.haml +++ b/app/views/admin/deploy_keys/index.html.haml @@ -2,7 +2,7 @@ .panel.panel-default .panel-heading Public deploy keys (#{@deploy_keys.count}) - .panel-head-actions + .controls = link_to 'New Deploy Key', new_admin_deploy_key_path, class: "btn btn-new btn-sm" - if @deploy_keys.any? .table-holder diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index d9b481404f7..b96ad6e2208 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -1,7 +1,7 @@ - page_title "Projects" = render 'shared/show_aside' -.row +.row.prepend-top-default %aside.col-md-3 .admin-filter = form_tag admin_namespaces_projects_path, method: :get, class: '' do @@ -47,7 +47,7 @@ .panel.panel-default .panel-heading Projects (#{@projects.total_count}) - .panel-head-actions + .controls .dropdown.inline %button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'} %span.light sort: diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index e2f97fd9337..3430f56a9c9 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -1,5 +1,4 @@ - header_title group_title(@group, "Settings", edit_group_path(@group)) -- @blank_container = true .panel.panel-default.prepend-top-default .panel-heading diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 6a8acc42af9..6b7fd5746d6 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -1,6 +1,5 @@ - page_title "Members" - header_title group_title(@group, "Members", group_group_members_path(@group)) -- @blank_container = true .group-members-page.prepend-top-default - if current_user && current_user.can?(:admin_group_member, @group) @@ -20,7 +19,7 @@ group members %small (#{@members.total_count}) - .pull-right + .controls = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do .form-group = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false } diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index 9ca11ed1177..dd75766121e 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -6,9 +6,9 @@ %strong= @group.name projects: - if can? current_user, :admin_group, @group - .panel-head-actions + .controls = link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do - %i.fa.fa-plus + = icon('plus') New Project %ul.well-list - @projects.each do |project| diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index a42fd38de3a..52bfc595fda 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -1,6 +1,5 @@ - page_title "Account" - header_title page_title, profile_account_path -- @blank_container = true - if current_user.ldap_user? .alert.alert-info diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml index 1c2458fa144..c53033e367c 100644 --- a/app/views/projects/project_members/_group_members.html.haml +++ b/app/views/projects/project_members/_group_members.html.haml @@ -5,7 +5,7 @@ %small (#{members.count}) - if can?(current_user, :admin_group_member, @group) - .pull-right + .controls = link_to group_group_members_path(@group), class: 'btn' do = icon('pencil-square-o') Manage group members diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index ccddab13aaf..e8dce30425f 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -4,7 +4,7 @@ project members %small (#{members.count}) - .pull-right + .controls = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do .form-group = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false } diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 6239a148905..0f8848a5cbe 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,13 +1,12 @@ - page_title "Members" = render "header_title" -- @blank_container = true .project-members-page.prepend-top-default - if can?(current_user, :admin_project_member, @project) .panel.panel-default .panel-heading Add new user to project - .pull-right + .controls = link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do Import members .panel-body -- cgit v1.2.1 From 7c520c7803524de0eefb8fbcc79d4c943a80c4de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Fri, 22 Jan 2016 17:00:35 -0500 Subject: Make sorting preference reusable for all projects. --- app/controllers/application_controller.rb | 21 +++++++++++++++------ app/models/concerns/cookie_helpers.rb | 5 ----- app/models/group.rb | 1 - app/models/project.rb | 1 - app/models/user.rb | 1 - features/dashboard/dashboard.feature | 14 ++++++++++++++ features/project/issues/issues.feature | 7 +++++++ features/project/merge_requests.feature | 7 +++++++ 8 files changed, 43 insertions(+), 14 deletions(-) delete mode 100644 app/models/concerns/cookie_helpers.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index fb5900054ce..5d047f09f64 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -409,13 +409,22 @@ class ApplicationController < ActionController::Base private def set_default_sort - # Use the controller name so we have a distinct cookie for Issues, MRs and Dashboard - cookie_suffix = "_sort_#{params[:controller].underscore}" + key = if is_a_listing_page_for?('issues') + 'issues_sort' + elsif is_a_listing_page_for?('merge_requests') + 'merge_requests_sort' + end - key = "#{(@project || @group || current_user).cookie_key}#{cookie_suffix}" + cookies[key] = params[:sort] if key && params[:sort].present? + params[:sort] = cookies[key] if key + params[:sort] ||= 'id_desc' + end + + def is_a_listing_page_for?(page_type) + controller_name, action_name = params.values_at(:controller, :action) - cookies[key] ||= 'id_desc' - cookies[key] = params[:sort] if params[:sort].present? - params[:sort] = cookies[key] + (controller_name == "projects/#{page_type}" && action_name == 'index') || + (controller_name == 'groups' && action_name == page_type) || + (controller_name == 'dashboard' && action_name == page_type) end end diff --git a/app/models/concerns/cookie_helpers.rb b/app/models/concerns/cookie_helpers.rb deleted file mode 100644 index 1c8d43a4661..00000000000 --- a/app/models/concerns/cookie_helpers.rb +++ /dev/null @@ -1,5 +0,0 @@ -module CookieHelpers - def cookie_key - "#{model_name.singular}_#{id}" - end -end diff --git a/app/models/group.rb b/app/models/group.rb index 3ba18b85193..76042b3e3fd 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -19,7 +19,6 @@ require 'file_size_validator' class Group < Namespace include Gitlab::ConfigHelper include Referable - include CookieHelpers has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember' alias_method :members, :group_members diff --git a/app/models/project.rb b/app/models/project.rb index 5ba7532c61a..5579710a476 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -51,7 +51,6 @@ class Project < ActiveRecord::Base include AfterCommitQueue include CaseSensitivity include TokenAuthenticatable - include CookieHelpers extend Gitlab::ConfigHelper diff --git a/app/models/user.rb b/app/models/user.rb index c24f8a35380..4214f01f6a4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -73,7 +73,6 @@ class User < ActiveRecord::Base include Sortable include CaseSensitivity include TokenAuthenticatable - include CookieHelpers add_authentication_token_field :authentication_token diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature index 9060e9d0ad2..c3b3577c449 100644 --- a/features/dashboard/dashboard.feature +++ b/features/dashboard/dashboard.feature @@ -50,6 +50,13 @@ Feature: Dashboard And I visit dashboard issues page Then The list should be sorted by "Oldest updated" + @javascript + Scenario: Visiting Project's issues after sorting + Given I visit dashboard issues page + And I sort the list by "Oldest updated" + And I visit project "Shop" issues page + Then The list should be sorted by "Oldest updated" + @javascript Scenario: Sorting Merge Requests Given I visit dashboard merge requests page @@ -57,3 +64,10 @@ Feature: Dashboard And I visit dashboard activity page And I visit dashboard merge requests page Then The list should be sorted by "Oldest updated" + + @javascript + Scenario: Visiting Project's merge requests after sorting + Given I visit dashboard merge requests page + And I sort the list by "Oldest updated" + And I visit project "Shop" merge requests page + Then The list should be sorted by "Oldest updated" diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index c1ae10f89c9..a91c5e0095d 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -67,6 +67,13 @@ Feature: Project Issues And I visit project "Shop" issues page Then The list should be sorted by "Oldest updated" + @javascript + Scenario: Visiting Merge Requests from a differente Project after sorting + Given I visit project "Shop" merge requests page + And I sort the list by "Oldest updated" + And I visit dashboard merge requests page + Then The list should be sorted by "Oldest updated" + @javascript Scenario: I search issue Given I fill in issue search with "Re" diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 55c17ad0c15..6b43e7ffedf 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -92,6 +92,13 @@ Feature: Project Merge Requests And I visit project "Shop" merge requests page Then The list should be sorted by "Oldest updated" + @javascript + Scenario: Visiting Merge Requests from a differente Project after sorting + Given I visit project "Shop" merge requests page + And I sort the list by "Oldest updated" + And I visit dashboard merge requests page + Then The list should be sorted by "Oldest updated" + @javascript Scenario: Visiting Merge Requests after commenting on diffs Given project "Shop" have "Bug NS-05" open merge request with diffs inside -- cgit v1.2.1 From c3c92c7317df157e156c3a34ff9f4c562229479f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Fri, 22 Jan 2016 18:43:21 -0500 Subject: Make default sorting preference work for Issues and MRs. --- app/controllers/application_controller.rb | 6 ++---- features/project/issues/issues.feature | 7 +++++++ features/project/merge_requests.feature | 7 +++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 540cc30bed5..824175c8a6c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -410,10 +410,8 @@ class ApplicationController < ActionController::Base private def set_default_sort - key = if is_a_listing_page_for?('issues') - 'issues_sort' - elsif is_a_listing_page_for?('merge_requests') - 'merge_requests_sort' + key = if is_a_listing_page_for?('issues') || is_a_listing_page_for?('merge_requests') + 'issuable_sort' end cookies[key] = params[:sort] if key && params[:sort].present? diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index a91c5e0095d..0b3d03aa2a5 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -67,6 +67,13 @@ Feature: Project Issues And I visit project "Shop" issues page Then The list should be sorted by "Oldest updated" + @javascript + Scenario: Visiting Merge Requests after being sorted the list + Given I visit project "Shop" issues page + And I sort the list by "Oldest updated" + And I visit project "Shop" merge requests page + Then The list should be sorted by "Oldest updated" + @javascript Scenario: Visiting Merge Requests from a differente Project after sorting Given I visit project "Shop" merge requests page diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 2938a22caa2..ca1ee6b3c2b 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -92,6 +92,13 @@ Feature: Project Merge Requests And I visit project "Shop" merge requests page Then The list should be sorted by "Oldest updated" + @javascript + Scenario: Visiting Issues after being sorted the list + Given I visit project "Shop" merge requests page + And I sort the list by "Oldest updated" + And I visit project "Shop" issues page + Then The list should be sorted by "Oldest updated" + @javascript Scenario: Visiting Merge Requests from a differente Project after sorting Given I visit project "Shop" merge requests page -- cgit v1.2.1 From 367cd8ac75eb53b0d7a94e9803fe2f881bd86313 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 22 Jan 2016 16:15:32 -0800 Subject: Remove gray background from issue/mr/milestone background Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/blocks.scss | 6 +++++- app/views/projects/issues/show.html.haml | 4 ++-- app/views/projects/merge_requests/_show.html.haml | 2 +- app/views/projects/merge_requests/show/_commits.html.haml | 2 +- app/views/projects/merge_requests/show/_mr_box.html.haml | 2 +- app/views/projects/milestones/show.html.haml | 15 +++++++-------- 6 files changed, 17 insertions(+), 14 deletions(-) diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index d0f5d33bf4d..bd89cc7dc1d 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -146,6 +146,10 @@ border-bottom: 1px solid $border-color; &.oneline-block { - line-height: 42px; + line-height: 36px; + } + + > .controls { + float: right; } } diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 7ed898ce72f..51dcca7a1ab 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -35,7 +35,7 @@ Edit .issue-details.issuable-details - .detail-page-description.gray-content-block.second-block + .detail-page-description.content-block %h2.title = markdown escape_once(@issue.title), pipeline: :single_line %div @@ -50,7 +50,7 @@ .merge-requests = render 'merge_requests' - .gray-content-block.second-block.oneline-block + .content-block = render 'votes/votes_block', votable: @issue .row diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 200bfa5ac4f..8641c3d8b4b 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -66,7 +66,7 @@ .tab-content #notes.notes.tab-pane.voting_notes - .gray-content-block.second-block.oneline-block + .content-block.oneline-block = render 'votes/votes_block', votable: @merge_request .row diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml index 7f904ec42a0..a8f09f855d4 100644 --- a/app/views/projects/merge_requests/show/_commits.html.haml +++ b/app/views/projects/merge_requests/show/_commits.html.haml @@ -1,4 +1,4 @@ -.gray-content-block.middle-block.oneline-block +.content-block.oneline-block = icon("sort-amount-desc") Most recent commits displayed first diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml index 0f81e5e8914..905823f79d9 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -1,4 +1,4 @@ -.detail-page-description.gray-content-block.second-block +.detail-page-description.content-block %h2.title = markdown escape_once(@merge_request.title), pipeline: :single_line diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 1142c584592..528a4f9552f 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -32,7 +32,7 @@ = icon('pencil-square-o') Edit -.detail-page-description.gray-content-block.second-block +.detail-page-description.content-block %h2.title = markdown escape_once(@milestone.title), pipeline: :single_line %div @@ -73,8 +73,8 @@ .tab-content .tab-pane.active#tab-issues - .gray-content-block.middle-block - .pull-right + .content-block.oneline-block + .controls - if can?(current_user, :create_issue, @project) = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do %i.fa.fa-plus @@ -94,8 +94,8 @@ = render('issues', title: 'Completed Issues (closed)', issues: @issues.closed, id: 'closed') .tab-pane#tab-merge-requests - .gray-content-block.middle-block - .pull-right + .content-block.oneline-block + .controls - if can?(current_user, :read_merge_request, @project) = link_to 'Browse Merge Requests', namespace_project_merge_requests_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped" @@ -117,9 +117,8 @@ = render 'merge_request', merge_request: merge_request .tab-pane#tab-participants - .gray-content-block.middle-block - .oneline - All participants to this milestone + .content-block.oneline-block + All participants to this milestone %ul.bordered-list - @users.each do |user| -- cgit v1.2.1 From 6435f78a8c66be92613c3a8ea4ec8171d0c38fea Mon Sep 17 00:00:00 2001 From: Benedict Etzel Date: Fri, 22 Jan 2016 13:47:14 +0100 Subject: Whitelist raw "abbr" elements when parsing Markdown Closes #12517 --- CHANGELOG | 1 + lib/banzai/filter/sanitization_filter.rb | 4 ++++ spec/lib/banzai/filter/sanitization_filter_spec.rb | 5 +++++ 3 files changed, 10 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 7af6a22f37f..d78c38cf1dc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ v 8.5.0 (unreleased) - Upgrade gitlab_git to 7.2.23 to fix commit message mentions in first branch push - New UI for pagination - Fix diff comments loaded by AJAX to load comment with diff in discussion tab + - Whitelist raw "abbr" elements when parsing Markdown (Benedict Etzel) v 8.4.0 - Allow LDAP users to change their email if it was not set by the LDAP server diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb index 3f49d492f2f..d1e11eedec3 100644 --- a/lib/banzai/filter/sanitization_filter.rb +++ b/lib/banzai/filter/sanitization_filter.rb @@ -43,6 +43,10 @@ module Banzai # Allow span elements whitelist[:elements].push('span') + # Allow abbr elements with title attribute + whitelist[:elements].push('abbr') + whitelist[:attributes]['abbr'] = %w(title) + # Allow any protocol in `a` elements... whitelist[:protocols].delete('a') diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb index 760d60a4190..9c63d227044 100644 --- a/spec/lib/banzai/filter/sanitization_filter_spec.rb +++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb @@ -75,6 +75,11 @@ describe Banzai::Filter::SanitizationFilter, lib: true do expect(filter(act).to_html).to eq exp end + it 'allows `abbr` elements' do + exp = act = %q{HTML} + expect(filter(act).to_html).to eq exp + end + it 'removes `rel` attribute from `a` elements' do act = %q{Link} exp = %q{Link} -- cgit v1.2.1 From 2c28c11df5496dac4804c2dedcb27c51ca50483e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 23 Jan 2016 21:29:43 +0100 Subject: Forcefully install apt packages --- scripts/prepare_build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index fb648d27621..b5d15847bca 100755 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -9,7 +9,7 @@ if [ -f /.dockerinit ]; then dpkg -i /cache/phantomjs_1.9.8-0jessie_amd64.deb apt-get update -qq - apt-get -o dir::cache::archives="/cache/apt" install -y -qq \ + apt-get -o dir::cache::archives="/cache/apt" install -y -qq --force-yes \ libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client cp config/database.yml.mysql config/database.yml -- cgit v1.2.1 From a7c4d0da8c7a340efd7b92718cd0f9436f2ec56f Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sat, 23 Jan 2016 16:08:15 -0800 Subject: Make the `/groups` route behave as expected The route is supposed to redirect the Groups#index request based on whether or not a user was logged in. If they are, we redirect them to their groups dashboard; if they're not, we redirect them to the public Explore page. But due to overly aggressive `before_action`s that weren't excluding the `index` action, the request always resulted in a 404, whether a user was logged in or not. Closes #12660 --- app/controllers/groups_controller.rb | 9 +++++---- spec/controllers/groups_controller_spec.rb | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 spec/controllers/groups_controller_spec.rb diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index fb26a4e6fc3..f7c9e619755 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -2,17 +2,18 @@ class GroupsController < Groups::ApplicationController include IssuesAction include MergeRequestsAction - skip_before_action :authenticate_user!, only: [:show, :issues, :merge_requests] respond_to :html - before_action :group, except: [:new, :create] + + skip_before_action :authenticate_user!, only: [:index, :show, :issues, :merge_requests] + before_action :group, except: [:index, :new, :create] # Authorize - before_action :authorize_read_group!, except: [:show, :new, :create, :autocomplete] + before_action :authorize_read_group!, except: [:index, :show, :new, :create, :autocomplete] before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects] before_action :authorize_create_group!, only: [:new, :create] # Load group projects - before_action :load_projects, except: [:new, :create, :projects, :edit, :update, :autocomplete] + before_action :load_projects, except: [:index, :new, :create, :projects, :edit, :update, :autocomplete] before_action :event_filter, only: :show layout :determine_layout diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb new file mode 100644 index 00000000000..938e97298b6 --- /dev/null +++ b/spec/controllers/groups_controller_spec.rb @@ -0,0 +1,23 @@ +require 'rails_helper' + +describe GroupsController do + describe 'GET index' do + context 'as a user' do + it 'redirects to Groups Dashboard' do + sign_in(create(:user)) + + get :index + + expect(response).to redirect_to(dashboard_groups_path) + end + end + + context 'as a guest' do + it 'redirects to Explore Groups' do + get :index + + expect(response).to redirect_to(explore_groups_path) + end + end + end +end -- cgit v1.2.1 From fdf68a8d4dec9000ec1e48f66ad64d63050685a1 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sat, 23 Jan 2016 16:44:46 -0800 Subject: Skip the 2FA requirement during logout --- app/controllers/sessions_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 825f85199be..44eb58e418b 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -2,6 +2,8 @@ class SessionsController < Devise::SessionsController include AuthenticatesWithTwoFactor include Recaptcha::ClientHelper + skip_before_action :check_2fa_requirement, only: [:destroy] + prepend_before_action :authenticate_with_two_factor, only: [:create] prepend_before_action :store_redirect_path, only: [:new] before_action :auto_sign_in_with_provider, only: [:new] -- cgit v1.2.1 From fcab1345da15020e5bf31055f52f9634fa8dd3ae Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sat, 23 Jan 2016 17:23:42 -0800 Subject: Update wording for 2FA requirement notice --- app/controllers/profiles/two_factor_auths_controller.rb | 4 ++-- spec/features/login_spec.rb | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 6e91d9b4ad9..f3bfede4354 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -13,10 +13,10 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController current_user.save! if current_user.changed? if two_factor_grace_period_expired? - flash.now[:alert] = 'You must configure Two-Factor Authentication in your account.' + flash.now[:alert] = 'You must enable Two-factor Authentication for your account.' else grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours - flash.now[:alert] = "You must configure Two-Factor Authentication in your account until #{l(grace_period_deadline)}." + flash.now[:alert] = "You must enable Two-factor Authentication for your account before #{l(grace_period_deadline)}." end @qr_code = build_qr_code diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index 2451e56fe7c..dac9205449a 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -112,10 +112,10 @@ feature 'Login', feature: true do context 'within the grace period' do it 'redirects to two-factor configuration page' do expect(current_path).to eq new_profile_two_factor_auth_path - expect(page).to have_content('You must configure Two-Factor Authentication in your account until') + expect(page).to have_content('You must enable Two-factor Authentication for your account before') end - it 'two-factor configuration is skippable' do + it 'disallows skipping two-factor configuration' do expect(current_path).to eq new_profile_two_factor_auth_path click_link 'Configure it later' @@ -128,10 +128,10 @@ feature 'Login', feature: true do it 'redirects to two-factor configuration page' do expect(current_path).to eq new_profile_two_factor_auth_path - expect(page).to have_content('You must configure Two-Factor Authentication in your account.') + expect(page).to have_content('You must enable Two-factor Authentication for your account.') end - it 'two-factor configuration is not skippable' do + it 'disallows skipping two-factor configuration' do expect(current_path).to eq new_profile_two_factor_auth_path expect(page).not_to have_link('Configure it later') end @@ -146,7 +146,7 @@ feature 'Login', feature: true do it 'redirects to two-factor configuration page' do expect(current_path).to eq new_profile_two_factor_auth_path - expect(page).to have_content('You must configure Two-Factor Authentication in your account.') + expect(page).to have_content('You must enable Two-factor Authentication for your account.') end end end -- cgit v1.2.1 From 2b3e64a6015bcb9a2bf365dcab148d5024833d91 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sun, 24 Jan 2016 19:28:24 +0100 Subject: Clean up namespaces API documentation [ci skip] --- doc/api/namespaces.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md index 68ed8bd3152..42d9ce3d391 100644 --- a/doc/api/namespaces.md +++ b/doc/api/namespaces.md @@ -1,22 +1,29 @@ # Namespaces -Namespaces account for usernames and groupnames. +Usernames and groupnames fall under a special category called namespaces. + +For users and groups supported API calls see the [users](users.md) and +[groups](groups.md) documentation respectively. [Pagination](README.md#pagination) is used. ## List namespaces -Get a list of namespaces. As a user, your namespaces are listed whereas if you -are an administrator you get a list of all namespaces in the GitLab instance. +Get a list of the namespaces of the authenticated user. If the user is an +administrator, a list of all namespaces in the GitLab instance is shown. ``` GET /namespaces ``` +Example request: + ```bash curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces ``` +Example response: + ```json [ { @@ -44,10 +51,14 @@ GET /namespaces?search=foobar | --------- | ---- | -------- | ----------- | | `search` | string | no | Returns a list of namespaces the user is authorized to see based on the search criteria | +Example request: + ```bash curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces?search=twitter ``` +Example response: + ```json [ { -- cgit v1.2.1 From 23a878c40ca8ca73d371c561bb4d2730551f3a38 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sun, 24 Jan 2016 20:19:46 +0100 Subject: Clean up system hooks API documentation [ci skip] --- doc/api/system_hooks.md | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md index b605f3540f3..dc036d7e27f 100644 --- a/doc/api/system_hooks.md +++ b/doc/api/system_hooks.md @@ -1,18 +1,24 @@ # System hooks -All methods require admin authorization. +All methods require administrator authorization. The URL endpoint of the system hooks can also be configured using the UI in -the admin area under hooks(`/admin/hooks`). +the admin area under **Hooks** (`/admin/hooks`). + +Read more about [system hooks](../system_hooks/system_hooks.md). ## List system hooks Get a list of all system hooks. +--- + ``` GET /hooks ``` +Example request: + ```bash curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks ``` @@ -31,6 +37,10 @@ Example response: ## Add new system hook +Add a new system hook. + +--- + ``` POST /hooks ``` @@ -39,6 +49,8 @@ POST /hooks | --------- | ---- | -------- | ----------- | | `url` | string | yes | The hook URL | +Example request: + ```bash curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/hooks?url=https://gitlab.example.com/hook" ``` @@ -65,6 +77,8 @@ GET /hooks/:id | --------- | ---- | -------- | ----------- | | `id` | integer | yes | The ID of the hook | +Example request: + ```bash curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2 ``` @@ -85,8 +99,12 @@ Example response: ## Delete system hook Deletes a system hook. This is an idempotent API function and returns `200 OK` -even if the hook is not available. If the hook is deleted a JSON object is -returned. +even if the hook is not available. + +If the hook is deleted, a JSON object is returned. An error is raised if the +hook is not found. + +--- ``` DELETE /hooks/:id @@ -96,6 +114,8 @@ DELETE /hooks/:id | --------- | ---- | -------- | ----------- | | `id` | integer | yes | The ID of the hook | +Example request: + ```bash curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2 ``` -- cgit v1.2.1 From 6f615500a98c3375d80f5d5aff5b76a8216db0ca Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sun, 24 Jan 2016 21:22:19 +0100 Subject: Move all integration docs images in a single directory [ci skip] --- doc/integration/facebook.md | 6 +++--- doc/integration/facebook_api_keys.png | Bin 125921 -> 0 bytes doc/integration/facebook_app_settings.png | Bin 134387 -> 0 bytes doc/integration/facebook_website_url.png | Bin 42292 -> 0 bytes doc/integration/github.md | 2 +- doc/integration/github_app.png | Bin 75297 -> 0 bytes doc/integration/gitlab.md | 2 +- doc/integration/gitlab_app.png | Bin 55325 -> 0 bytes doc/integration/gmail_action_buttons_for_gitlab.md | 2 +- doc/integration/gmail_actions_button.png | Bin 17321 -> 0 bytes doc/integration/google.md | 2 +- doc/integration/google_app.png | Bin 52669 -> 0 bytes doc/integration/img/facebook_api_keys.png | Bin 0 -> 125921 bytes doc/integration/img/facebook_app_settings.png | Bin 0 -> 134387 bytes doc/integration/img/facebook_website_url.png | Bin 0 -> 42292 bytes doc/integration/img/github_app.png | Bin 0 -> 75297 bytes doc/integration/img/gitlab_app.png | Bin 0 -> 55325 bytes .../img/gmail_action_buttons_for_gitlab.png | Bin 0 -> 17321 bytes doc/integration/img/google_app.png | Bin 0 -> 52669 bytes .../img/oauth_provider_admin_application.png | Bin 0 -> 55533 bytes .../img/oauth_provider_application_form.png | Bin 0 -> 25075 bytes .../img/oauth_provider_authorized_application.png | Bin 0 -> 17260 bytes .../img/oauth_provider_user_wide_applications.png | Bin 0 -> 46238 bytes doc/integration/img/twitter_app_api_keys.png | Bin 0 -> 72200 bytes doc/integration/img/twitter_app_details.png | Bin 0 -> 121621 bytes doc/integration/jira-integration-points.png | Bin 67854 -> 0 bytes doc/integration/oauth_provider.md | 8 ++++---- doc/integration/oauth_provider/admin_application.png | Bin 55533 -> 0 bytes doc/integration/oauth_provider/application_form.png | Bin 25075 -> 0 bytes .../oauth_provider/authorized_application.png | Bin 17260 -> 0 bytes .../oauth_provider/user_wide_applications.png | Bin 46238 -> 0 bytes doc/integration/twitter.md | 6 +++--- doc/integration/twitter_app_api_keys.png | Bin 72200 -> 0 bytes doc/integration/twitter_app_details.png | Bin 121621 -> 0 bytes 34 files changed, 14 insertions(+), 14 deletions(-) delete mode 100644 doc/integration/facebook_api_keys.png delete mode 100644 doc/integration/facebook_app_settings.png delete mode 100644 doc/integration/facebook_website_url.png delete mode 100644 doc/integration/github_app.png delete mode 100644 doc/integration/gitlab_app.png delete mode 100644 doc/integration/gmail_actions_button.png delete mode 100644 doc/integration/google_app.png create mode 100644 doc/integration/img/facebook_api_keys.png create mode 100644 doc/integration/img/facebook_app_settings.png create mode 100644 doc/integration/img/facebook_website_url.png create mode 100644 doc/integration/img/github_app.png create mode 100644 doc/integration/img/gitlab_app.png create mode 100644 doc/integration/img/gmail_action_buttons_for_gitlab.png create mode 100644 doc/integration/img/google_app.png create mode 100644 doc/integration/img/oauth_provider_admin_application.png create mode 100644 doc/integration/img/oauth_provider_application_form.png create mode 100644 doc/integration/img/oauth_provider_authorized_application.png create mode 100644 doc/integration/img/oauth_provider_user_wide_applications.png create mode 100644 doc/integration/img/twitter_app_api_keys.png create mode 100644 doc/integration/img/twitter_app_details.png delete mode 100644 doc/integration/jira-integration-points.png delete mode 100644 doc/integration/oauth_provider/admin_application.png delete mode 100644 doc/integration/oauth_provider/application_form.png delete mode 100644 doc/integration/oauth_provider/authorized_application.png delete mode 100644 doc/integration/oauth_provider/user_wide_applications.png delete mode 100644 doc/integration/twitter_app_api_keys.png delete mode 100644 doc/integration/twitter_app_details.png diff --git a/doc/integration/facebook.md b/doc/integration/facebook.md index bc1f1673086..77bb75cbfca 100644 --- a/doc/integration/facebook.md +++ b/doc/integration/facebook.md @@ -19,7 +19,7 @@ something else descriptive. 1. Enter the address of your GitLab installation at the bottom of the package - ![Facebook Website URL](facebook_website_url.png) + ![Facebook Website URL](img/facebook_website_url.png) 1. Choose "Next" @@ -29,7 +29,7 @@ something else descriptive. 1. Fill in a contact email for your app - ![Facebook App Settings](facebook_app_settings.png) + ![Facebook App Settings](img/facebook_app_settings.png) 1. Choose "Save Changes" @@ -45,7 +45,7 @@ something else descriptive. 1. You should now see an app key and app secret (see screenshot). Keep this page open as you continue configuration. - ![Facebook API Keys](facebook_api_keys.png) + ![Facebook API Keys](img/facebook_api_keys.png) 1. On your GitLab server, open the configuration file. diff --git a/doc/integration/facebook_api_keys.png b/doc/integration/facebook_api_keys.png deleted file mode 100644 index d6c44ac0f11..00000000000 Binary files a/doc/integration/facebook_api_keys.png and /dev/null differ diff --git a/doc/integration/facebook_app_settings.png b/doc/integration/facebook_app_settings.png deleted file mode 100644 index 30dd21e198a..00000000000 Binary files a/doc/integration/facebook_app_settings.png and /dev/null differ diff --git a/doc/integration/facebook_website_url.png b/doc/integration/facebook_website_url.png deleted file mode 100644 index dc3088bb2fa..00000000000 Binary files a/doc/integration/facebook_website_url.png and /dev/null differ diff --git a/doc/integration/github.md b/doc/integration/github.md index a789d2c814f..886784a27c9 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -22,7 +22,7 @@ GitHub will generate an application ID and secret key for you to use. 1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). Keep this page open as you continue configuration. - ![GitHub app](github_app.png) + ![GitHub app](img/github_app.png) 1. On your GitLab server, open the configuration file. diff --git a/doc/integration/github_app.png b/doc/integration/github_app.png deleted file mode 100644 index d890345ced9..00000000000 Binary files a/doc/integration/github_app.png and /dev/null differ diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md index 80e3c0142a0..b215cc7c609 100644 --- a/doc/integration/gitlab.md +++ b/doc/integration/gitlab.md @@ -28,7 +28,7 @@ GitLab.com will generate an application ID and secret key for you to use. 1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). Keep this page open as you continue configuration. - ![GitLab app](gitlab_app.png) + ![GitLab app](img/gitlab_app.png) 1. On your GitLab server, open the configuration file. diff --git a/doc/integration/gitlab_app.png b/doc/integration/gitlab_app.png deleted file mode 100644 index 3f9391a821b..00000000000 Binary files a/doc/integration/gitlab_app.png and /dev/null differ diff --git a/doc/integration/gmail_action_buttons_for_gitlab.md b/doc/integration/gmail_action_buttons_for_gitlab.md index de45f25ad62..05a91d9bef9 100644 --- a/doc/integration/gmail_action_buttons_for_gitlab.md +++ b/doc/integration/gmail_action_buttons_for_gitlab.md @@ -4,7 +4,7 @@ GitLab supports [Google actions in email](https://developers.google.com/gmail/ma If correctly setup, emails that require an action will be marked in Gmail. -![gmail_actions_button.png](gmail_actions_button.png) +![gmail_actions_button.png](img/gmail_action_buttons_for_gitlab.png) To get this functioning, you need to be registered with Google. [See how to register with Google in this document.](https://developers.google.com/gmail/markup/registering-with-google) diff --git a/doc/integration/gmail_actions_button.png b/doc/integration/gmail_actions_button.png deleted file mode 100644 index b08f54d137b..00000000000 Binary files a/doc/integration/gmail_actions_button.png and /dev/null differ diff --git a/doc/integration/google.md b/doc/integration/google.md index 91e9b2495cc..f9a20dd840d 100644 --- a/doc/integration/google.md +++ b/doc/integration/google.md @@ -25,7 +25,7 @@ To enable the Google OAuth2 OmniAuth provider you must register your application - Application type: "Web Application" - Authorized JavaScript origins: This isn't really used by GitLab but go ahead and put 'https://gitlab.example.com' here. - Authorized redirect URI: 'https://gitlab.example.com/users/auth/google_oauth2/callback' -1. Under the heading "Client ID for web application" you should see a Client ID and Client secret (see screenshot). Keep this page open as you continue configuration. ![Google app](google_app.png) +1. Under the heading "Client ID for web application" you should see a Client ID and Client secret (see screenshot). Keep this page open as you continue configuration. ![Google app](img/google_app.png) 1. On your GitLab server, open the configuration file. diff --git a/doc/integration/google_app.png b/doc/integration/google_app.png deleted file mode 100644 index 5a62ad35009..00000000000 Binary files a/doc/integration/google_app.png and /dev/null differ diff --git a/doc/integration/img/facebook_api_keys.png b/doc/integration/img/facebook_api_keys.png new file mode 100644 index 00000000000..d6c44ac0f11 Binary files /dev/null and b/doc/integration/img/facebook_api_keys.png differ diff --git a/doc/integration/img/facebook_app_settings.png b/doc/integration/img/facebook_app_settings.png new file mode 100644 index 00000000000..30dd21e198a Binary files /dev/null and b/doc/integration/img/facebook_app_settings.png differ diff --git a/doc/integration/img/facebook_website_url.png b/doc/integration/img/facebook_website_url.png new file mode 100644 index 00000000000..dc3088bb2fa Binary files /dev/null and b/doc/integration/img/facebook_website_url.png differ diff --git a/doc/integration/img/github_app.png b/doc/integration/img/github_app.png new file mode 100644 index 00000000000..d890345ced9 Binary files /dev/null and b/doc/integration/img/github_app.png differ diff --git a/doc/integration/img/gitlab_app.png b/doc/integration/img/gitlab_app.png new file mode 100644 index 00000000000..3f9391a821b Binary files /dev/null and b/doc/integration/img/gitlab_app.png differ diff --git a/doc/integration/img/gmail_action_buttons_for_gitlab.png b/doc/integration/img/gmail_action_buttons_for_gitlab.png new file mode 100644 index 00000000000..b08f54d137b Binary files /dev/null and b/doc/integration/img/gmail_action_buttons_for_gitlab.png differ diff --git a/doc/integration/img/google_app.png b/doc/integration/img/google_app.png new file mode 100644 index 00000000000..5a62ad35009 Binary files /dev/null and b/doc/integration/img/google_app.png differ diff --git a/doc/integration/img/oauth_provider_admin_application.png b/doc/integration/img/oauth_provider_admin_application.png new file mode 100644 index 00000000000..a5f34512aa8 Binary files /dev/null and b/doc/integration/img/oauth_provider_admin_application.png differ diff --git a/doc/integration/img/oauth_provider_application_form.png b/doc/integration/img/oauth_provider_application_form.png new file mode 100644 index 00000000000..ae135db2627 Binary files /dev/null and b/doc/integration/img/oauth_provider_application_form.png differ diff --git a/doc/integration/img/oauth_provider_authorized_application.png b/doc/integration/img/oauth_provider_authorized_application.png new file mode 100644 index 00000000000..d3ce05be9cc Binary files /dev/null and b/doc/integration/img/oauth_provider_authorized_application.png differ diff --git a/doc/integration/img/oauth_provider_user_wide_applications.png b/doc/integration/img/oauth_provider_user_wide_applications.png new file mode 100644 index 00000000000..719e1974068 Binary files /dev/null and b/doc/integration/img/oauth_provider_user_wide_applications.png differ diff --git a/doc/integration/img/twitter_app_api_keys.png b/doc/integration/img/twitter_app_api_keys.png new file mode 100644 index 00000000000..1076337172a Binary files /dev/null and b/doc/integration/img/twitter_app_api_keys.png differ diff --git a/doc/integration/img/twitter_app_details.png b/doc/integration/img/twitter_app_details.png new file mode 100644 index 00000000000..b95e8af8a74 Binary files /dev/null and b/doc/integration/img/twitter_app_details.png differ diff --git a/doc/integration/jira-integration-points.png b/doc/integration/jira-integration-points.png deleted file mode 100644 index 0692a7b458a..00000000000 Binary files a/doc/integration/jira-integration-points.png and /dev/null differ diff --git a/doc/integration/oauth_provider.md b/doc/integration/oauth_provider.md index 192c321f712..dbe5a175c82 100644 --- a/doc/integration/oauth_provider.md +++ b/doc/integration/oauth_provider.md @@ -15,16 +15,16 @@ GitLab has two ways to add new OAuth2 application to an instance, you can add ap ### Adding application through profile Go to your profile section 'Application' and press button 'New Application' -![applications](oauth_provider/user_wide_applications.png) +![applications](img/oauth_provider_user_wide_applications.png) After this you will see application form, where "Name" is arbitrary name, "Redirect URI" is URL in your app where users will be sent after authorization on GitLab.com. -![application_form](oauth_provider/application_form.png) +![application_form](img/oauth_provider_application_form.png) ### Authorized application Every application you authorized will be shown in your "Authorized application" sections. -![authorized_application](oauth_provider/authorized_application.png) +![authorized_application](img/oauth_provider_authorized_application.png) At any time you can revoke access just clicking button "Revoke" @@ -32,4 +32,4 @@ At any time you can revoke access just clicking button "Revoke" If you want to create application that does not belong to certain user you can create it from admin area -![admin_application](oauth_provider/admin_application.png) \ No newline at end of file +![admin_application](img/oauth_provider_admin_application.png) diff --git a/doc/integration/oauth_provider/admin_application.png b/doc/integration/oauth_provider/admin_application.png deleted file mode 100644 index a5f34512aa8..00000000000 Binary files a/doc/integration/oauth_provider/admin_application.png and /dev/null differ diff --git a/doc/integration/oauth_provider/application_form.png b/doc/integration/oauth_provider/application_form.png deleted file mode 100644 index ae135db2627..00000000000 Binary files a/doc/integration/oauth_provider/application_form.png and /dev/null differ diff --git a/doc/integration/oauth_provider/authorized_application.png b/doc/integration/oauth_provider/authorized_application.png deleted file mode 100644 index d3ce05be9cc..00000000000 Binary files a/doc/integration/oauth_provider/authorized_application.png and /dev/null differ diff --git a/doc/integration/oauth_provider/user_wide_applications.png b/doc/integration/oauth_provider/user_wide_applications.png deleted file mode 100644 index 719e1974068..00000000000 Binary files a/doc/integration/oauth_provider/user_wide_applications.png and /dev/null differ diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md index 52ed4a22339..4769f26b259 100644 --- a/doc/integration/twitter.md +++ b/doc/integration/twitter.md @@ -14,7 +14,7 @@ To enable the Twitter OmniAuth provider you must register your application with - Callback URL: 'https://gitlab.example.com/users/auth/twitter/callback' - Agree to the "Developer Agreement". - ![Twitter App Details](twitter_app_details.png) + ![Twitter App Details](img/twitter_app_details.png) 1. Select "Create your Twitter application." 1. Select the "Settings" tab. @@ -27,7 +27,7 @@ To enable the Twitter OmniAuth provider you must register your application with 1. You should now see an API key and API secret (see screenshot). Keep this page open as you continue configuration. - ![Twitter app](twitter_app_api_keys.png) + ![Twitter app](img/twitter_app_api_keys.png) 1. On your GitLab server, open the configuration file. @@ -76,4 +76,4 @@ To enable the Twitter OmniAuth provider you must register your application with 1. Restart GitLab for the changes to take effect. -On the sign in page there should now be a Twitter icon below the regular sign in form. Click the icon to begin the authentication process. Twitter will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in. \ No newline at end of file +On the sign in page there should now be a Twitter icon below the regular sign in form. Click the icon to begin the authentication process. Twitter will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in. diff --git a/doc/integration/twitter_app_api_keys.png b/doc/integration/twitter_app_api_keys.png deleted file mode 100644 index 1076337172a..00000000000 Binary files a/doc/integration/twitter_app_api_keys.png and /dev/null differ diff --git a/doc/integration/twitter_app_details.png b/doc/integration/twitter_app_details.png deleted file mode 100644 index b95e8af8a74..00000000000 Binary files a/doc/integration/twitter_app_details.png and /dev/null differ -- cgit v1.2.1 From 040fb46dc777995155d6d2a234365508b9b3124b Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sun, 24 Jan 2016 21:36:49 +0100 Subject: Move supported omniauth providers to the top [ci skip] --- doc/integration/omniauth.md | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index e9e17eb4165..8e6627b2be5 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -9,6 +9,23 @@ Configuring OmniAuth does not prevent standard GitLab authentication or LDAP (if - [Enable OmniAuth for an Existing User](#enable-omniauth-for-an-existing-user) - [OmniAuth configuration sample when using Omnibus GitLab](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master#omniauth-google-twitter-github-login) +## Supported Providers + +This is a list of the current supported OmniAuth providers. Before proceeding +on each provider's documentation, make sure to first read this document as it +contains some settings that are common for all providers. + +- [GitHub](github.md) +- [Bitbucket](bitbucket.md) +- [GitLab.com](gitlab.md) +- [Google](google.md) +- [Facebook](facebook.md) +- [Twitter](twitter.md) +- [Shibboleth](shibboleth.md) +- [SAML](saml.md) +- [Crowd](crowd.md) +- [Azure](azure.md) + ## Initial OmniAuth Configuration Before configuring individual OmniAuth providers there are a few global settings that are in common for all providers that we need to consider. @@ -67,19 +84,6 @@ If you want to change these settings: Now we can choose one or more of the Supported Providers below to continue configuration. -## Supported Providers - -- [GitHub](github.md) -- [Bitbucket](bitbucket.md) -- [GitLab.com](gitlab.md) -- [Google](google.md) -- [Facebook](facebook.md) -- [Twitter](twitter.md) -- [Shibboleth](shibboleth.md) -- [SAML](saml.md) -- [Crowd](crowd.md) -- [Azure](azure.md) - ## Enable OmniAuth for an Existing User Existing users can enable OmniAuth for specific providers after the account is created. For example, if the user originally signed in with LDAP an OmniAuth provider such as Twitter can be enabled. Follow the steps below to enable an OmniAuth provider for an existing user. -- cgit v1.2.1 From 5c69560a2d7a3b0db2db6d0b57baa677fd21f494 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sun, 24 Jan 2016 22:49:37 +0100 Subject: Add note on artifacts browser functionality Based on the following comment: https://gitlab.com/gitlab-org/gitlab-ce/issues/12634#note_3359006 [ci skip] --- doc/ci/build_artifacts/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/ci/build_artifacts/README.md b/doc/ci/build_artifacts/README.md index b112beef7b0..58cbe653c34 100644 --- a/doc/ci/build_artifacts/README.md +++ b/doc/ci/build_artifacts/README.md @@ -11,6 +11,11 @@ Starting from GitLab 8.4 and GitLab Runner 1.0, the artifacts archive format changed to `ZIP`, and it is now possible to browse its contents, with the added ability of downloading the files separately. +**Note:** +The artifacts browser will be available only for new artifacts that are sent +to GitLab using GitLab Runner version 1.0 and up. You will not be available to +see the browser for old artifacts already uploaded to GitLab. + ## Enabling build artifacts _If you are searching for ways to use artifacts, jump to -- cgit v1.2.1 From 2e911721d2d288a4b2015a55dedd19b2313a452f Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 24 Jan 2016 15:06:46 -0800 Subject: Update text_color_for_bg helper to support RGB triplet color codes Closes #12677 --- app/helpers/labels_helper.rb | 6 +++++- spec/helpers/labels_helper_spec.rb | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index a2c3d4d2f32..92eac0560bd 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -83,7 +83,11 @@ module LabelsHelper end def text_color_for_bg(bg_color) - r, g, b = bg_color.slice(1,7).scan(/.{2}/).map(&:hex) + if bg_color.length == 4 + r, g, b = bg_color[1, 4].scan(/./).map { |v| (v * 2).hex } + else + r, g, b = bg_color[1, 7].scan(/.{2}/).map(&:hex) + end if (r + g + b) > 500 '#333333' diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index 0c8d06b7059..0b9176357bc 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -66,5 +66,10 @@ describe LabelsHelper do it 'uses dark text on light backgrounds' do expect(text_color_for_bg('#EEEEEE')).to eq('#333333') end + + it 'supports RGB triplets' do + expect(text_color_for_bg('#FFF')).to eq '#333333' + expect(text_color_for_bg('#000')).to eq '#FFFFFF' + end end end -- cgit v1.2.1 From 0ae588255cc224e8704f714843cf488949c11426 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 24 Jan 2016 15:27:50 -0800 Subject: Remove link from system hook Closes #12672 --- app/views/admin/hooks/index.html.haml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index b120f4dea67..53b3cd04c68 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -37,8 +37,7 @@ - @hooks.each do |hook| %li .list-item-name - = link_to admin_hook_path(hook) do - %strong= hook.url + %strong= hook.url %p SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} .pull-right -- cgit v1.2.1 From d3f24a22219c1706ff580cd00ad9fa519d40a9c9 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Thu, 21 Jan 2016 21:18:03 -0500 Subject: Adds base64 background search icon. All inputs of type `search` will have the gray background and search icon centered. Because the search magnifier is a bg image, I had to hide it with `[value=""]`. I added a little javascript to make sure each input always has it's own value. --- app/assets/javascripts/application.js.coffee | 9 +++++++ app/assets/stylesheets/framework/forms.scss | 33 +++++++++++++++++++++++- app/assets/stylesheets/framework/header.scss | 6 ----- app/assets/stylesheets/pages/issues.scss | 5 ---- app/helpers/application_helper.rb | 2 +- app/views/shared/issuable/_search_form.html.haml | 2 +- features/steps/project/wiki.rb | 2 +- 7 files changed, 44 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index eb18d32b25c..48c9890cfb5 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -203,4 +203,13 @@ $ -> form = btn.closest("form") new ConfirmDangerModal(form, text) + $('input[type="search"]').each -> + $this = $(this) + $this.attr 'value', $this.val() + return + + $(document).on 'keyup', 'input[type="search"]' , (e) -> + $this = $(this) + $this.attr 'value', $this.val() + new Aside() diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 4dab806d50e..685dfa21ea5 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -2,13 +2,43 @@ textarea { resize: vertical; } +input { + border-radius: 3px; +} + +input[type='search'], input[type='search'].search-text-input { - background-image: image-url("icon-search.png"); background-repeat: no-repeat; background-position: 10px; + background-size: 16px; + background-position-x: 30%; padding-left: 25px; + background-color: $gray-light; + + &[value=""] { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAW0UlEQVR4nO19e7BlV1nn7/vWWnufx312p29wSgYdIRFFSSCDkwRIYBpJVBxwinEsnMHHMJAXNUI5pTXlKA5/DFMaCgkhYFlEy0eRIhU1OEQiMMlEoRkx0aCMqKQQSMzt7vu+5+zHWt83f6y97z19053cs88+93ac/lXte/v2eey19m+t9T3Xt+iJJ57AfqCqUFUQEVQVzAxVBQCICJgZRAQRQQgBRAQigjEGRIQQAgCAiMBMAAAmQukFRRlgDD/Xh/BiVXxXlnsB0YtV9VoRhaqKAODdzxvDlBPRnznLDxrmRwF9jAlfSRIHIgCq8CG2GQSYqn11m+NXEQAoziPYg7yZYYIxjCBAXvjLSx+uKL1cWZThUlG8TFWtqIKJoFAQqCIWIMQnJ6LwQSDxwT4PwBu4IpkIj6YufNYa/lzi+PPOmr8kovq9AHYHFgAw81jtrwicKmjaM8RaA2sMQgjICv9deRH+beHlXxY+fI9I/LxhAhAffBNo9UOh2P1ORuL4j6zhz3RTe1e34/6WQMiLsm4b6ch02X0iFMk/y0sHgakRwkywxoCYMcz9ywdZeWNe+B/xQcFcjegpdkwVEI0EWcvSSey9vdT+Wiex9zITQjXL6lFPALBnaQXiLBIRiMiBkDQVQggKVbislLcMcn9jXvjvBOp1fHqdOXfbAR8ETEAndff3OvZ9vdT9lTG84YOcBiJ5TITgPYIInHM7/XzWEmItQ0WxNSy+fzvztxZluIRAMOYQWDgHfBAAQJoYme+nn+2m9uUiisIHsDFA9fCttYdCyHhS7RwgApxlFEW4/PR69nurm8XHSy+XWMPnFRkAYA3DGkZeBD65Orj69PrwE0H0uYkzU11C94uJCFFUmhMz1rfyH1teHXxhOyt/0BqqBPX5C2sYzIzNQXndkyuDR7aH5WudNWCmQ9WDJyLEGUYQPXpqPbtndTP/CAjkbCuT7kBQz2xVPbKynt13en34YVVle4iDqdHTIyI4y8jLcOmTq4MTWeFf7yyDD0NitwCu7KPN7fwtJ1cH/1uUjllzOANr7LsSxem+uV28eXl18KgE/bbDanybqGdLGfSqU+vDLw3z8vhhzPax7kiIZGxs5289tT68kwB3vgntSVEvw8urg/u3huXrDpqUfd+NABjDWN8u3ry6md9hzK5P6h8TakWFiXBqffj7m4PiuLPmwO6/L0KiC4SxsV3cuLqR3WkNPWvlxX7BHDXFU2uD+7eGxeuc4QNxQz4jIYo4jbeG/t+vbeUfsIam6mRTBULlQAwS3RshSPW37rxW/z1NW40pDryV9eHvZ2W41rnpL1/0jW9842nf4KxBVvgXnFwbfJloOjMjPtj4ZK3hLWfNI9bQloieENVta43h+D5VQAj4doU+PwS1pZergkTru/b6TqN9zLR58ZHec5loPVTW/jRgz+WCVlUYJgTRb1rZyD6lilaNPQVQdyx19mudxHzMWv6MZXrYGP56mjhkeYlBVqLXS2AJyMvoRU4s77jiQ9Dv8CKvKstw+bAIryt9WKrlXVuwhlB6mT29nt17bL77Smba8Sq3jXP6sqgK6iyvDk4MsvJlbWobPggIQLfjHp7puncnznzcMBfxtYAQonMvLzyGeYlux8EQUHg5gxCoYlTg+iAzhZdXbA/Ltw3z8geBqBW2hdIL5vrJuxdn058r/XRmCT3++ONnfcFFIX7DykZ2e1tk1EtTr+M+NtNNfrmbms8R0W5kD9hx4u2HkDiLd52cTNGRKaIY5v7VW8Py54e5f6WpBPSkYzrKN8FF892Xd1Pzx2XVnjZxVhliDcMHvWh5dfAPCjVtyI3SCxLLfzk/2/n5mW5yt1bCeu9DmoSQUdTL63ZW/uT6VvELQeSb25gtQRTW8JePLXQuJQBtr1znbOH6dn6bFzFmQjJUFWUQzPTcnUuLvZf2EnN3kKglTVOLrMO8c/30155zpP+STmI/Wfow8T0NE4rSX7Kd+bc7Z2OEscWLjTEYvdLUIS/lukFW/rAzPFEHpEo0WOinP31sof/jRMjzMkxVVR2FapyZxtDJY4u91872kveFMHlcwxjG5qD8xWHul5hiP7Wli+vgS71UhCDY2M5/ZdKHIaJQAY7Od9+5MJv+UqjshsOwJ8uKhIsWev9pYaZz66ia3QRMhBBkfnOQf8Qww7Q5Q85ghxSDYXl9VvgXTLLeiipEFUfnOz8923O3jmSJHAoIu9kqi7PpOxdm0g/4IBPNVGsZWR6+Lyv8Zcxob4ac8R+iGOTlf5tkEKvGzh+d672j17G/VBzgEvVMiKFawVw/vXl+Jr3VS3PVtSZ5kPlb4l/tTP2daWCYkJVyeV6El/IEsyOIYK6f/NxM177Xh/OEiRGoRvfL4mz3nbPd5FcnsSeMIQxz/8YyqLOWQTz5xRx/wFqDrAw3BdFGXBMA7wXd1N6/MJO+27cgPKcFVYWIYHE2/Y9pYr7oG7pCiAhBZHaYle8gKFRk4otjuiej9HJJXvifsA3jG0EUxvJwcbbz5jonqhZUAA4sa2O/8JUBsTiT/itmatw2IkJW+J8Joim3IdSB6GoeZP7NRRnqfNexIapYmEn/izP8RJQbir0aXN2Bg0R9770XVOG9IE3sV+Z66buaLq+GCaWXhaKUK9vwnzFiUhtKH17W9GGFIOgk9iu91L7XS52ZyGe99iQ8Tx3nagczgys7a6bnfjFx/LXQ0OwWVQzy8l+30l4iQlH4i/IyXDmJN3em695C1dR/JmJHE56nif2omd4HMJH0O+5ntSEhVRb/66KdNdkKwIYZpZfrfZB+ky8LQZAm5rFu6j4tomOtl9PAuPeIglnR7ya/lTjz1SZu9eggleeVPlxreEIZIirwQV7QpPMAoCD0O8l7mKl1R1uj9ozIqnEGhrOMNLV3NVm2iKJNkhX+ymiHSeOLoYTChyubjFdVhTUUksT8togAE1iobaB+wE3uL6Lod9xvWsONDVkRfGu0EanxxV5kofRybZPQZxBFJ7Eft8ybTQVijUlJmZTcEATW0F84y480cfNUy9alPghUIsFNLhuCXKYK29T0Txzfx0yYYph5X5hUe1MAFgRn+b6s8JeN+zyYAB/kMlX9JufME01DvFYUqUIbJS8wkTDR50NLRt/uCN9/W9rW2BJn/qSJwlEpB3PeyyXO8BOizUaoLUpfYrxnAGAnCWLoLP+tatsGX72j8OnfAbRr00js05eZCLFP432eCdjO/CCbwKFqATouquAxGREFnOWBMewjoe0QEonVHdn4dO9hArSdLS4AotUtjBUibKnqTJNlnBlHE0ONww1WRP9Fkw9WM+RPAAxEFNq2WVE5J8+YedU/vQ9Tia9UcugkE/0fr/qqcbsURJFYc8Vs393X1BVjRTVvsv0yTmt9PCv81HKUgJgTpSPrhw8BRR3xm4JxyVH7HDb9vKh2VZsvpZYA0QZCRAAwcydxFkGk9e1gOy0iQr0gWyZYNmO3dRzEJIbMaQmgWY51Ock4sfvxPZ0V1efibqNp5rzuDham6W6lBiIhRLFwwbjEqwJMDGMMpOEabomoUVZ3FZzRYeExqVF4bmjsYJXoVvpQqbnTulsMRYhoaGIGxMmsO9mXTWBBZJuMBorpL0UbaTXnvkflPa7yj0mjI9DacXXC/d8PAFSbDe+qvUZC9Es1geWGfVMBmOgF/Y5rX+PR3V95sZvcVv92JsZb2s60q2b9oqhe3uip6O5+xcZ2iCH6QwCvGfeDREAI8hJRTQmUt0nKzkg9xyiTKoWHQK3Kd459OqqKpQm+9i+CAE3zOywzP9xsvST4oP3Sy9HUmcdbHa0jmtXZX4+/tF0+av/ArDRUdKpl/KtFGctzNIG1hgI1cBVQdKYlIcj3cOruaeq7mQSty64Y+XuFiKJRsgcBieUksYwgzYYKA/giEXyTBVkBBNFXtmWfNXEUthaFrLTNvAyvaqJcRxL5H5w1/1cxXpRw9LKG6TQT/b1X/WfjNoOJkBXh9UXpf0oFle4+PkYfZJOHWttSk5BSqbt978P3NvkKUYU1/CXDfCqaAQ3bYQ0jScxnmrg/DBMKH77FB73cGG48KoDJlp/RSGFTxMHlf6AM2msSrNO45W+DCCAoiJpdNk4T+mLTjogohkX40U5iHh7HFqqJIKLGOvte1DOlWek+xWBY3tSUUyLAWfMJZp7IUGYRgWX+bNPdq4YJg2HxH0ov3XFmyTSMyabxdCYgL8PVWRle0SQVKuYWsDhD95TeT5bkoCCkiT3hjPlS0xSYIDo3yPx7XFWB9DDTgAAghADv/b6TK1QVG9v5O6Xh/hURwDD9tbW83DS3qwYTERJnkDi+p+lUYyZsDotbsqL8TqanT4NpO9PkbBiVS89EvDWEYV5eNczKNzTd3Cqq6Kbud2O9LcbeXWnjXFynwHRSe9eYS+8O4o4ixcag+ADzU0f/qLyY9uzYi9FBMIpIVpSB61vFnU2Hh2oktZva35EqkNE0FUq1in8GUfRS9+edxP1p09R8Zxnbw/KarUH5Fmf5KfKiLY1qXIzeazcHK/bRMGF9O39PXjbfMRZEkDj7hLX8aJCzJ3aPJ88qK52J0EnMByd5VsyE1c3sw3kRnn8+1tDa7Xgc1YPcX7O+lf/nSStU9DvuXdH91MJ2hBACRAKK0qOTmLud5bJpSLbO1ji1PvwDUVxk7WS7eKcBVSBxBj7oC1fWs7snWUJFFIkzX++k5kNNfVd7sTOMRRXOmvVex/3KJHq0MQQf5JJTa4MHVfTiSbdWtwlFXFp9kMtOrg0fCCJHm9ZXJMSlvpva2wkx8SKEya8zap0wE3yQ+eWV7VMK2EkqOMTNMOarxxZ7V0H18VAti8CZNYBrw/AM90fc1goRQVEKrDUwDBRlLNmUOgJRJLr+zr22TV1LeFTTqorpvHh5dfDHZZB+YpvHLaqKDsOlhe4/JcKptoKmZ+zCDUGQWF7vd5NfnbQEkbWMrAjPW14ZfFoU/yQ5xGqlRDvL1IuWVwef9kH6yQRBJCAuVzNd9wtJYk+BnmZT0LjXXqEiCsz2kp9JnPn7SdN7nGUUPly6vDp4OMvDv6m1r4NCHSO3hrE9LG9aXh2cCEGOTLqM+iDoJOYLMz33P4piMsv8qZb6HrXLB4E1vDHXT29uIwpoDSOILC2vbn90bTO72UQX88Tfux8k1oBAWNnI3rW8OrhNVHtmQjI0ZmmGuZn030GfufjMuCr+U54MISaj9VJ7b+rM3WWYPOfKMFcqcf7+J1e3H8qKcLWzseR32xOGaLec+PaweP3y2uDRzUHxX63hVgqwiSoWZ9N391L3pbjH/Uw7Z/Tftfzaa5g+o2G4F/VnF2bSNxqitTbSfIhi8eWs8FefXBs8dGo9u3OY+xdzNWMmOb6CKArsSDDZQe7fcHoj+4NT68N7ylJe1CbxBEBFv1mhqD0bo6rzU+yKOmNmj5finH8/Xc3FxBpsDcvXnN4YfrLNoyZUAS8CE/1oJ9LE/G43MR81zI8ZEysaqESNKi+ilmUZyEtBkIBOwmDiuOOIYsGyIPrtg6x8U1b4NxVevrXywLbT4D0ovWB+Jr1tYSa5xQcBUbxPrdnFPsZBzMxnHHBTv29Ulo5qgs9YBDOxxqxsZn+1sZ1f0nb9WgUgQaGIh65Y5r9JnLk3ceZ/mbij96+9169Zy2wYVHjVEEScxfMB+pYgcGUZri+DXOGDvCQ+nOkfBKAay3PM9ZLbF2bTmxRU7YBqgZBzlfirYZgB6PypteEnh7l/mZuS9a2qEN11+lVVEYSA5Z0hGM81EgWec+b7qUqSPkANTmMtsPl+8uHF+e5bQxCEcACE1J1VIH1yZfBQUforEmumbn3H/lTxirotIz9j26bciGeAalSA5mc6H1ycTW8sfdgZO00J2dci62NBmvzYQvdViTMnigPYUEi0K+jqgsbx791Nq4eNqNEZbGznN6xu5u+PAbrJvnN/pcYRBZmzvHXxkf41iTWfKw97l+d5glrDW9/Kb17ZzO8wZ4kHjYP9F+Onqn4hc37xYu/a1Jk/m1bt2mcbqFLdN7azt65t5e9zExxUMLZe6IOAmPJjC92r+113T+HPr7JLh4XaIF3byt++spG9z1S21bhopKhXUcXsovnODy3Odn42Fsl/ds+WNsYUUdzAtLaVv31lffhBUzkMx0GzI49wRlHJ/37xkZkrrOFHSj9ZYcnDgOpuxe02fHf18rW+nb9tdTN7P9N4NQAmO6VNo1zppuYLS4v9y2f7yZ11keRnA3y1sWZhJr3p2GLvh6GxxMakChzFpDmsb+U3n14b3m7GOG+lFd9CLAEOXDTf/fGlxd41ncT+Xn2+x/mIEOLs7nXcQ8cWe9fMdO3tvY6769hC7zgxBd8GKaiOhxoUN6xtFXcYs79D0/ZlGO6N8I16L0ct03oN9SLYHpY3DjL/U3kZns/U7vERTVHX6k2dOTHXTz7U7yYfUVWUPsRCoIaQl3L8yZXt+1H5wiYdUtF4FCzMJO9dnOu9IwRBGcIZ5IxtqY9DiKpCRVDFHWxWhJ/cHpY356V/UZ3dcpBnV1WORxAB3dSe6HeTWzuO72LmncOLVXePkk0Siyzz1y2vbn9CVVsZSDEaq5ibSW9bnO3c4sOZpTemTshO3pMxSJxBCIphUf7zYe5vyAt/3Ht5bh0Pr63vtqCKHQFNAKzljU7i7u4k/OtpYh+wxuzs5h3tS01IPVOGuX/tybXBvSLq2vAa77hZ+ukdi3OdG0Yrax8YIaOFL2OKPqEovcmLcH1Whh/xXl7rRY6GoLuuEmDfJNU5Vqq7O1OYCNby45bpi53E3t3tuI9Zyysqu0tT3fZzESIisIZQlPLq5bXBpyRmfIzHwNnai5j8MT+T3LY407nFi+y0/0AJAXYdaqrxKKUq7XLeB/3uwofv8F5eGkQuCaIvDEGPiKp9Sm+gVYQoEsZMK8zkrOG/Y8IjzPx3ncQ+4Cx/nqB5fMhnnl6wX0JUFYmzyMtw3fJKi8sXokyZ6yUfOjLXeVsI0YY7NELq12IHDZgYzjGywsdnrdotfHgOFFcpkKqqaMUDEVW7xcgB+jdM/LCzxgJymqr7EXMsTlPdbzR9dVxC4vLFyEt/zcnVwf0i6tqSKT4o5mfSOxZmkhtC2J3h9mk/OWWoKoIK4KP+b4hAsQbXY86Yx0bftzfcWR+pR4iJ3qoKVoC13dzh0gd0EvvAxUdmjj95euuBKglkou8kIlhLWN/K3gaoX5zt3uJDgOh0i5SMjTpzvPYCPN0VguxoSNMEEVWebnrw6EL3+5gIbdop61vFzWtb+S9bY6IG2kaj/39A6QWp409cfKR/3Bj2bWTj1G6W1c3hOzYHxRucNRcIGQc+KFJnPnVsvnt9azOFYph8fSv7zaL0Ry8QMiZyH+Ac/9HSYu96ZpZ28tYIQbQ3yMobLhAyJmIioSJx5r6lxe6rjeGyjegpgZCX8voLhDRADGkHWEMPLC30rmtj+SIGgsixC4RMgMILEmc+vXSkdz0z7RwS0xQEhAuETIC4fAlSZ+5bWuhdQwT1Io18cxpPlvjyBUJaQFEGpIl58OLF/vUMgm+Q/CEKdBL74AVCWkIZd4z94dKR/vfTmDKlqiSEXsf9zgVCWkRFyv9cOtI7bpjDfrSvnTzhfvJD1tBjFwhpGd4LOon91NJi7zWG+Wsx9nL29wZRlF6wMJv+aL9j7wlBLljq00DpBdbQZ5YWu9/d7yYfiIc0j/jivNQ71b6xdKT/xvl++luhqgJxqN7ef8wog8AQ1o7OdW7Oy/AbeRne5L28UAFvmL+eOP5oJ7EPOWvysiqUQ0T4f6vtnYxDT2jjAAAAAElFTkSuQmCC +'); + } + + &::-webkit-input-placeholder { + text-align: center; + } + + &:-moz-placeholder { /* Firefox 18- */ + text-align: center; + } + + &::-moz-placeholder { /* Firefox 19+ */ + text-align: center; + } + + &:-ms-input-placeholder { + text-align: center; + } } + + input[type='text'].danger { background: #F2DEDE!important; border-color: #D66; @@ -74,6 +104,7 @@ label { .form-control { @include box-shadow(none); + border-radius: 3px; } .form-control-inline { diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index ba5e72c8c5a..f875b1460e7 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -108,16 +108,10 @@ header { .search-input { width: 220px; - background-image: image-url("icon-search.png"); - background-repeat: no-repeat; - background-position: 195px; - @include input-big; &:focus { @include box-shadow(none); outline: none; - border-color: #DDD; - background-color: #FFF; } } } diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index ad92cc22815..dd6a251f811 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -49,11 +49,6 @@ .issue-search-form { margin: 0; height: 24px; - - .issue_search { - border: 1px solid #DDD !important; - background-color: #f4f4f4; - } } form.edit-issue { diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f3a2723ee0d..a2458ad3be0 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -171,7 +171,7 @@ module ApplicationHelper def search_placeholder if @project && @project.persisted? - 'Search in this project' + 'Search' elsif @snippet || @snippets || @show_snippets 'Search snippets' elsif @group && @group.persisted? diff --git a/app/views/shared/issuable/_search_form.html.haml b/app/views/shared/issuable/_search_form.html.haml index 3a5ad00aa91..da1ccf9c963 100644 --- a/app/views/shared/issuable/_search_form.html.haml +++ b/app/views/shared/issuable/_search_form.html.haml @@ -1,6 +1,6 @@ = form_tag(path, method: :get, id: "issue_search_form", class: 'pull-left issue-search-form') do .append-right-10.hidden-xs.hidden-sm - = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input', spellcheck: false } + = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter', class: 'form-control issue_search search-text-input', spellcheck: false } = hidden_field_tag :state, params['state'] = hidden_field_tag :scope, params['scope'] = hidden_field_tag :assignee_id, params['assignee_id'] diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb index d753ae14590..2a735afbe7b 100644 --- a/features/steps/project/wiki.rb +++ b/features/steps/project/wiki.rb @@ -163,7 +163,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I search for Wiki content' do - fill_in "Search in this project", with: "wiki_content" + fill_in "Search", with: "wiki_content" click_button "Search" end -- cgit v1.2.1 From 75c93bb20d3bef042926b8896bf24138bb9ce554 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Thu, 21 Jan 2016 21:29:56 -0500 Subject: Make base64 image smaller by 5000 characters. --- app/assets/stylesheets/framework/forms.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 685dfa21ea5..cf8eccdbc40 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -16,8 +16,7 @@ input[type='search'].search-text-input { background-color: $gray-light; &[value=""] { - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAW0UlEQVR4nO19e7BlV1nn7/vWWnufx312p29wSgYdIRFFSSCDkwRIYBpJVBxwinEsnMHHMJAXNUI5pTXlKA5/DFMaCgkhYFlEy0eRIhU1OEQiMMlEoRkx0aCMqKQQSMzt7vu+5+zHWt83f6y97z19053cs88+93ac/lXte/v2eey19m+t9T3Xt+iJJ57AfqCqUFUQEVQVzAxVBQCICJgZRAQRQQgBRAQigjEGRIQQAgCAiMBMAAAmQukFRRlgDD/Xh/BiVXxXlnsB0YtV9VoRhaqKAODdzxvDlBPRnznLDxrmRwF9jAlfSRIHIgCq8CG2GQSYqn11m+NXEQAoziPYg7yZYYIxjCBAXvjLSx+uKL1cWZThUlG8TFWtqIKJoFAQqCIWIMQnJ6LwQSDxwT4PwBu4IpkIj6YufNYa/lzi+PPOmr8kovq9AHYHFgAw81jtrwicKmjaM8RaA2sMQgjICv9deRH+beHlXxY+fI9I/LxhAhAffBNo9UOh2P1ORuL4j6zhz3RTe1e34/6WQMiLsm4b6ch02X0iFMk/y0sHgakRwkywxoCYMcz9ywdZeWNe+B/xQcFcjegpdkwVEI0EWcvSSey9vdT+Wiex9zITQjXL6lFPALBnaQXiLBIRiMiBkDQVQggKVbislLcMcn9jXvjvBOp1fHqdOXfbAR8ETEAndff3OvZ9vdT9lTG84YOcBiJ5TITgPYIInHM7/XzWEmItQ0WxNSy+fzvztxZluIRAMOYQWDgHfBAAQJoYme+nn+2m9uUiisIHsDFA9fCttYdCyHhS7RwgApxlFEW4/PR69nurm8XHSy+XWMPnFRkAYA3DGkZeBD65Orj69PrwE0H0uYkzU11C94uJCFFUmhMz1rfyH1teHXxhOyt/0BqqBPX5C2sYzIzNQXndkyuDR7aH5WudNWCmQ9WDJyLEGUYQPXpqPbtndTP/CAjkbCuT7kBQz2xVPbKynt13en34YVVle4iDqdHTIyI4y8jLcOmTq4MTWeFf7yyDD0NitwCu7KPN7fwtJ1cH/1uUjllzOANr7LsSxem+uV28eXl18KgE/bbDanybqGdLGfSqU+vDLw3z8vhhzPax7kiIZGxs5289tT68kwB3vgntSVEvw8urg/u3huXrDpqUfd+NABjDWN8u3ry6md9hzK5P6h8TakWFiXBqffj7m4PiuLPmwO6/L0KiC4SxsV3cuLqR3WkNPWvlxX7BHDXFU2uD+7eGxeuc4QNxQz4jIYo4jbeG/t+vbeUfsIam6mRTBULlQAwS3RshSPW37rxW/z1NW40pDryV9eHvZ2W41rnpL1/0jW9842nf4KxBVvgXnFwbfJloOjMjPtj4ZK3hLWfNI9bQloieENVta43h+D5VQAj4doU+PwS1pZergkTru/b6TqN9zLR58ZHec5loPVTW/jRgz+WCVlUYJgTRb1rZyD6lilaNPQVQdyx19mudxHzMWv6MZXrYGP56mjhkeYlBVqLXS2AJyMvoRU4s77jiQ9Dv8CKvKstw+bAIryt9WKrlXVuwhlB6mT29nt17bL77Smba8Sq3jXP6sqgK6iyvDk4MsvJlbWobPggIQLfjHp7puncnznzcMBfxtYAQonMvLzyGeYlux8EQUHg5gxCoYlTg+iAzhZdXbA/Ltw3z8geBqBW2hdIL5vrJuxdn058r/XRmCT3++ONnfcFFIX7DykZ2e1tk1EtTr+M+NtNNfrmbms8R0W5kD9hx4u2HkDiLd52cTNGRKaIY5v7VW8Py54e5f6WpBPSkYzrKN8FF892Xd1Pzx2XVnjZxVhliDcMHvWh5dfAPCjVtyI3SCxLLfzk/2/n5mW5yt1bCeu9DmoSQUdTL63ZW/uT6VvELQeSb25gtQRTW8JePLXQuJQBtr1znbOH6dn6bFzFmQjJUFWUQzPTcnUuLvZf2EnN3kKglTVOLrMO8c/30155zpP+STmI/Wfow8T0NE4rSX7Kd+bc7Z2OEscWLjTEYvdLUIS/lukFW/rAzPFEHpEo0WOinP31sof/jRMjzMkxVVR2FapyZxtDJY4u91872kveFMHlcwxjG5qD8xWHul5hiP7Wli+vgS71UhCDY2M5/ZdKHIaJQAY7Od9+5MJv+UqjshsOwJ8uKhIsWev9pYaZz66ia3QRMhBBkfnOQf8Qww7Q5Q85ghxSDYXl9VvgXTLLeiipEFUfnOz8923O3jmSJHAoIu9kqi7PpOxdm0g/4IBPNVGsZWR6+Lyv8Zcxob4ac8R+iGOTlf5tkEKvGzh+d672j17G/VBzgEvVMiKFawVw/vXl+Jr3VS3PVtSZ5kPlb4l/tTP2daWCYkJVyeV6El/IEsyOIYK6f/NxM177Xh/OEiRGoRvfL4mz3nbPd5FcnsSeMIQxz/8YyqLOWQTz5xRx/wFqDrAw3BdFGXBMA7wXd1N6/MJO+27cgPKcFVYWIYHE2/Y9pYr7oG7pCiAhBZHaYle8gKFRk4otjuiej9HJJXvifsA3jG0EUxvJwcbbz5jonqhZUAA4sa2O/8JUBsTiT/itmatw2IkJW+J8Joim3IdSB6GoeZP7NRRnqfNexIapYmEn/izP8RJQbir0aXN2Bg0R9770XVOG9IE3sV+Z66buaLq+GCaWXhaKUK9vwnzFiUhtKH17W9GGFIOgk9iu91L7XS52ZyGe99iQ8Tx3nagczgys7a6bnfjFx/LXQ0OwWVQzy8l+30l4iQlH4i/IyXDmJN3em695C1dR/JmJHE56nif2omd4HMJH0O+5ntSEhVRb/66KdNdkKwIYZpZfrfZB+ky8LQZAm5rFu6j4tomOtl9PAuPeIglnR7ya/lTjz1SZu9eggleeVPlxreEIZIirwQV7QpPMAoCD0O8l7mKl1R1uj9ozIqnEGhrOMNLV3NVm2iKJNkhX+ymiHSeOLoYTChyubjFdVhTUUksT8togAE1iobaB+wE3uL6Lod9xvWsONDVkRfGu0EanxxV5kofRybZPQZxBFJ7Eft8ybTQVijUlJmZTcEATW0F84y480cfNUy9alPghUIsFNLhuCXKYK29T0Txzfx0yYYph5X5hUe1MAFgRn+b6s8JeN+zyYAB/kMlX9JufME01DvFYUqUIbJS8wkTDR50NLRt/uCN9/W9rW2BJn/qSJwlEpB3PeyyXO8BOizUaoLUpfYrxnAGAnCWLoLP+tatsGX72j8OnfAbRr00js05eZCLFP432eCdjO/CCbwKFqATouquAxGREFnOWBMewjoe0QEonVHdn4dO9hArSdLS4AotUtjBUibKnqTJNlnBlHE0ONww1WRP9Fkw9WM+RPAAxEFNq2WVE5J8+YedU/vQ9Tia9UcugkE/0fr/qqcbsURJFYc8Vs393X1BVjRTVvsv0yTmt9PCv81HKUgJgTpSPrhw8BRR3xm4JxyVH7HDb9vKh2VZsvpZYA0QZCRAAwcydxFkGk9e1gOy0iQr0gWyZYNmO3dRzEJIbMaQmgWY51Ock4sfvxPZ0V1efibqNp5rzuDham6W6lBiIhRLFwwbjEqwJMDGMMpOEabomoUVZ3FZzRYeExqVF4bmjsYJXoVvpQqbnTulsMRYhoaGIGxMmsO9mXTWBBZJuMBorpL0UbaTXnvkflPa7yj0mjI9DacXXC/d8PAFSbDe+qvUZC9Es1geWGfVMBmOgF/Y5rX+PR3V95sZvcVv92JsZb2s60q2b9oqhe3uip6O5+xcZ2iCH6QwCvGfeDREAI8hJRTQmUt0nKzkg9xyiTKoWHQK3Kd459OqqKpQm+9i+CAE3zOywzP9xsvST4oP3Sy9HUmcdbHa0jmtXZX4+/tF0+av/ArDRUdKpl/KtFGctzNIG1hgI1cBVQdKYlIcj3cOruaeq7mQSty64Y+XuFiKJRsgcBieUksYwgzYYKA/giEXyTBVkBBNFXtmWfNXEUthaFrLTNvAyvaqJcRxL5H5w1/1cxXpRw9LKG6TQT/b1X/WfjNoOJkBXh9UXpf0oFle4+PkYfZJOHWttSk5BSqbt978P3NvkKUYU1/CXDfCqaAQ3bYQ0jScxnmrg/DBMKH77FB73cGG48KoDJlp/RSGFTxMHlf6AM2msSrNO45W+DCCAoiJpdNk4T+mLTjogohkX40U5iHh7HFqqJIKLGOvte1DOlWek+xWBY3tSUUyLAWfMJZp7IUGYRgWX+bNPdq4YJg2HxH0ov3XFmyTSMyabxdCYgL8PVWRle0SQVKuYWsDhD95TeT5bkoCCkiT3hjPlS0xSYIDo3yPx7XFWB9DDTgAAghADv/b6TK1QVG9v5O6Xh/hURwDD9tbW83DS3qwYTERJnkDi+p+lUYyZsDotbsqL8TqanT4NpO9PkbBiVS89EvDWEYV5eNczKNzTd3Cqq6Kbud2O9LcbeXWnjXFynwHRSe9eYS+8O4o4ixcag+ADzU0f/qLyY9uzYi9FBMIpIVpSB61vFnU2Hh2oktZva35EqkNE0FUq1in8GUfRS9+edxP1p09R8Zxnbw/KarUH5Fmf5KfKiLY1qXIzeazcHK/bRMGF9O39PXjbfMRZEkDj7hLX8aJCzJ3aPJ88qK52J0EnMByd5VsyE1c3sw3kRnn8+1tDa7Xgc1YPcX7O+lf/nSStU9DvuXdH91MJ2hBACRAKK0qOTmLud5bJpSLbO1ji1PvwDUVxk7WS7eKcBVSBxBj7oC1fWs7snWUJFFIkzX++k5kNNfVd7sTOMRRXOmvVex/3KJHq0MQQf5JJTa4MHVfTiSbdWtwlFXFp9kMtOrg0fCCJHm9ZXJMSlvpva2wkx8SKEya8zap0wE3yQ+eWV7VMK2EkqOMTNMOarxxZ7V0H18VAti8CZNYBrw/AM90fc1goRQVEKrDUwDBRlLNmUOgJRJLr+zr22TV1LeFTTqorpvHh5dfDHZZB+YpvHLaqKDsOlhe4/JcKptoKmZ+zCDUGQWF7vd5NfnbQEkbWMrAjPW14ZfFoU/yQ5xGqlRDvL1IuWVwef9kH6yQRBJCAuVzNd9wtJYk+BnmZT0LjXXqEiCsz2kp9JnPn7SdN7nGUUPly6vDp4OMvDv6m1r4NCHSO3hrE9LG9aXh2cCEGOTLqM+iDoJOYLMz33P4piMsv8qZb6HrXLB4E1vDHXT29uIwpoDSOILC2vbn90bTO72UQX88Tfux8k1oBAWNnI3rW8OrhNVHtmQjI0ZmmGuZn030GfufjMuCr+U54MISaj9VJ7b+rM3WWYPOfKMFcqcf7+J1e3H8qKcLWzseR32xOGaLec+PaweP3y2uDRzUHxX63hVgqwiSoWZ9N391L3pbjH/Uw7Z/Tftfzaa5g+o2G4F/VnF2bSNxqitTbSfIhi8eWs8FefXBs8dGo9u3OY+xdzNWMmOb6CKArsSDDZQe7fcHoj+4NT68N7ylJe1CbxBEBFv1mhqD0bo6rzU+yKOmNmj5finH8/Xc3FxBpsDcvXnN4YfrLNoyZUAS8CE/1oJ9LE/G43MR81zI8ZEysaqESNKi+ilmUZyEtBkIBOwmDiuOOIYsGyIPrtg6x8U1b4NxVevrXywLbT4D0ovWB+Jr1tYSa5xQcBUbxPrdnFPsZBzMxnHHBTv29Ulo5qgs9YBDOxxqxsZn+1sZ1f0nb9WgUgQaGIh65Y5r9JnLk3ceZ/mbij96+9169Zy2wYVHjVEEScxfMB+pYgcGUZri+DXOGDvCQ+nOkfBKAay3PM9ZLbF2bTmxRU7YBqgZBzlfirYZgB6PypteEnh7l/mZuS9a2qEN11+lVVEYSA5Z0hGM81EgWec+b7qUqSPkANTmMtsPl+8uHF+e5bQxCEcACE1J1VIH1yZfBQUforEmumbn3H/lTxirotIz9j26bciGeAalSA5mc6H1ycTW8sfdgZO00J2dci62NBmvzYQvdViTMnigPYUEi0K+jqgsbx791Nq4eNqNEZbGznN6xu5u+PAbrJvnN/pcYRBZmzvHXxkf41iTWfKw97l+d5glrDW9/Kb17ZzO8wZ4kHjYP9F+Onqn4hc37xYu/a1Jk/m1bt2mcbqFLdN7azt65t5e9zExxUMLZe6IOAmPJjC92r+113T+HPr7JLh4XaIF3byt++spG9z1S21bhopKhXUcXsovnODy3Odn42Fsl/ds+WNsYUUdzAtLaVv31lffhBUzkMx0GzI49wRlHJ/37xkZkrrOFHSj9ZYcnDgOpuxe02fHf18rW+nb9tdTN7P9N4NQAmO6VNo1zppuYLS4v9y2f7yZ11keRnA3y1sWZhJr3p2GLvh6GxxMakChzFpDmsb+U3n14b3m7GOG+lFd9CLAEOXDTf/fGlxd41ncT+Xn2+x/mIEOLs7nXcQ8cWe9fMdO3tvY6769hC7zgxBd8GKaiOhxoUN6xtFXcYs79D0/ZlGO6N8I16L0ct03oN9SLYHpY3DjL/U3kZns/U7vERTVHX6k2dOTHXTz7U7yYfUVWUPsRCoIaQl3L8yZXt+1H5wiYdUtF4FCzMJO9dnOu9IwRBGcIZ5IxtqY9DiKpCRVDFHWxWhJ/cHpY356V/UZ3dcpBnV1WORxAB3dSe6HeTWzuO72LmncOLVXePkk0Siyzz1y2vbn9CVVsZSDEaq5ibSW9bnO3c4sOZpTemTshO3pMxSJxBCIphUf7zYe5vyAt/3Ht5bh0Pr63vtqCKHQFNAKzljU7i7u4k/OtpYh+wxuzs5h3tS01IPVOGuX/tybXBvSLq2vAa77hZ+ukdi3OdG0Yrax8YIaOFL2OKPqEovcmLcH1Whh/xXl7rRY6GoLuuEmDfJNU5Vqq7O1OYCNby45bpi53E3t3tuI9Zyysqu0tT3fZzESIisIZQlPLq5bXBpyRmfIzHwNnai5j8MT+T3LY407nFi+y0/0AJAXYdaqrxKKUq7XLeB/3uwofv8F5eGkQuCaIvDEGPiKp9Sm+gVYQoEsZMK8zkrOG/Y8IjzPx3ncQ+4Cx/nqB5fMhnnl6wX0JUFYmzyMtw3fJKi8sXokyZ6yUfOjLXeVsI0YY7NELq12IHDZgYzjGywsdnrdotfHgOFFcpkKqqaMUDEVW7xcgB+jdM/LCzxgJymqr7EXMsTlPdbzR9dVxC4vLFyEt/zcnVwf0i6tqSKT4o5mfSOxZmkhtC2J3h9mk/OWWoKoIK4KP+b4hAsQbXY86Yx0bftzfcWR+pR4iJ3qoKVoC13dzh0gd0EvvAxUdmjj95euuBKglkou8kIlhLWN/K3gaoX5zt3uJDgOh0i5SMjTpzvPYCPN0VguxoSNMEEVWebnrw6EL3+5gIbdop61vFzWtb+S9bY6IG2kaj/39A6QWp409cfKR/3Bj2bWTj1G6W1c3hOzYHxRucNRcIGQc+KFJnPnVsvnt9azOFYph8fSv7zaL0Ry8QMiZyH+Ac/9HSYu96ZpZ28tYIQbQ3yMobLhAyJmIioSJx5r6lxe6rjeGyjegpgZCX8voLhDRADGkHWEMPLC30rmtj+SIGgsixC4RMgMILEmc+vXSkdz0z7RwS0xQEhAuETIC4fAlSZ+5bWuhdQwT1Io18cxpPlvjyBUJaQFEGpIl58OLF/vUMgm+Q/CEKdBL74AVCWkIZd4z94dKR/vfTmDKlqiSEXsf9zgVCWkRFyv9cOtI7bpjDfrSvnTzhfvJD1tBjFwhpGd4LOon91NJi7zWG+Wsx9nL29wZRlF6wMJv+aL9j7wlBLljq00DpBdbQZ5YWu9/d7yYfiIc0j/jivNQ71b6xdKT/xvl++luhqgJxqN7ef8wog8AQ1o7OdW7Oy/AbeRne5L28UAFvmL+eOP5oJ7EPOWvysiqUQ0T4f6vtnYxDT2jjAAAAAElFTkSuQmCC -'); + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC'); } &::-webkit-input-placeholder { -- cgit v1.2.1 From dd8e9a28e79ab0b39ee91fd888c5709635546440 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Thu, 21 Jan 2016 21:32:55 -0500 Subject: Removes extra spaces --- app/assets/stylesheets/framework/forms.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index cf8eccdbc40..f4e9e1515f0 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -36,8 +36,6 @@ input[type='search'].search-text-input { } } - - input[type='text'].danger { background: #F2DEDE!important; border-color: #D66; -- cgit v1.2.1 From c10705c59fa25adf6d7bfb48abf5e60f0b5bbf68 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Fri, 22 Jan 2016 08:15:56 -0500 Subject: Distinguishes between search inputs. Main search input has different styles than secondary search inputs. --- app/assets/stylesheets/framework/forms.scss | 12 ++++++------ app/views/shared/issuable/_search_form.html.haml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index f4e9e1515f0..d4d1d7a700a 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -12,26 +12,26 @@ input[type='search'].search-text-input { background-position: 10px; background-size: 16px; background-position-x: 30%; - padding-left: 25px; + padding-left: 10px; background-color: $gray-light; - &[value=""] { + &.search-input[value=""] { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC'); } - &::-webkit-input-placeholder { + &.search-input::-webkit-input-placeholder { text-align: center; } - &:-moz-placeholder { /* Firefox 18- */ + &.search-input:-moz-placeholder { /* Firefox 18- */ text-align: center; } - &::-moz-placeholder { /* Firefox 19+ */ + &.search-input::-moz-placeholder { /* Firefox 19+ */ text-align: center; } - &:-ms-input-placeholder { + &.search-input:-ms-input-placeholder { text-align: center; } } diff --git a/app/views/shared/issuable/_search_form.html.haml b/app/views/shared/issuable/_search_form.html.haml index da1ccf9c963..6672ea79629 100644 --- a/app/views/shared/issuable/_search_form.html.haml +++ b/app/views/shared/issuable/_search_form.html.haml @@ -1,6 +1,6 @@ = form_tag(path, method: :get, id: "issue_search_form", class: 'pull-left issue-search-form') do .append-right-10.hidden-xs.hidden-sm - = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter', class: 'form-control issue_search search-text-input', spellcheck: false } + = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by name ...', class: 'form-control issue_search search-text-input', spellcheck: false } = hidden_field_tag :state, params['state'] = hidden_field_tag :scope, params['scope'] = hidden_field_tag :assignee_id, params['assignee_id'] -- cgit v1.2.1 From a80d55b766d84c488bbfc9b4e0c5330fc618d74e Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Fri, 22 Jan 2016 23:48:15 -0500 Subject: Border radius to vars. Only top search is gray. --- app/assets/stylesheets/framework/forms.scss | 10 +++++++--- app/assets/stylesheets/framework/tw_bootstrap_variables.scss | 6 +++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index d4d1d7a700a..d097e4d32f7 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -3,11 +3,15 @@ textarea { } input { - border-radius: 3px; + border-radius: $border-radius-base; +} + +input[type='search'] { + background-color: white; + padding-left: 10px; } -input[type='search'], -input[type='search'].search-text-input { +input[type='search'].search-input { background-repeat: no-repeat; background-position: 10px; background-size: 16px; diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index 798cd224ad0..33270388e64 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -22,9 +22,9 @@ $brand-info: $gl-info; $brand-warning: $gl-warning; $brand-danger: $gl-danger; -$border-radius-base: 2px !default; -$border-radius-large: 2px !default; -$border-radius-small: 2px !default; +$border-radius-base: 3px !default; +$border-radius-large: 3px !default; +$border-radius-small: 3px !default; //== Scaffolding -- cgit v1.2.1 From 3629984b96553cced12855fd4bc096d5268abd6d Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 25 Jan 2016 10:08:11 +0100 Subject: Move JIRA from integration to project_services --- doc/integration/README.md | 2 +- doc/integration/external-issue-tracker.md | 2 +- doc/integration/img/jira_issue_reference.png | Bin 39942 -> 0 bytes doc/integration/img/jira_merge_request_close.png | Bin 111150 -> 0 bytes doc/integration/img/jira_project_name.png | Bin 60598 -> 0 bytes doc/integration/img/jira_service.png | Bin 59082 -> 0 bytes doc/integration/img/jira_service_close_issue.png | Bin 88433 -> 0 bytes doc/integration/img/jira_service_page.png | Bin 35496 -> 0 bytes doc/integration/img/jira_workflow_screenshot.png | Bin 121534 -> 0 bytes doc/integration/jira.md | 150 +-------------------- doc/project_services/img/jira_issue_reference.png | Bin 0 -> 39942 bytes .../img/jira_merge_request_close.png | Bin 0 -> 111150 bytes doc/project_services/img/jira_project_name.png | Bin 0 -> 60598 bytes doc/project_services/img/jira_service.png | Bin 0 -> 59082 bytes .../img/jira_service_close_issue.png | Bin 0 -> 88433 bytes doc/project_services/img/jira_service_page.png | Bin 0 -> 35496 bytes .../img/jira_workflow_screenshot.png | Bin 0 -> 121534 bytes doc/project_services/project_services.md | 2 +- 18 files changed, 5 insertions(+), 151 deletions(-) delete mode 100644 doc/integration/img/jira_issue_reference.png delete mode 100644 doc/integration/img/jira_merge_request_close.png delete mode 100644 doc/integration/img/jira_project_name.png delete mode 100644 doc/integration/img/jira_service.png delete mode 100644 doc/integration/img/jira_service_close_issue.png delete mode 100644 doc/integration/img/jira_service_page.png delete mode 100644 doc/integration/img/jira_workflow_screenshot.png create mode 100644 doc/project_services/img/jira_issue_reference.png create mode 100644 doc/project_services/img/jira_merge_request_close.png create mode 100644 doc/project_services/img/jira_project_name.png create mode 100644 doc/project_services/img/jira_service.png create mode 100644 doc/project_services/img/jira_service_close_issue.png create mode 100644 doc/project_services/img/jira_service_page.png create mode 100644 doc/project_services/img/jira_workflow_screenshot.png diff --git a/doc/integration/README.md b/doc/integration/README.md index 846526f4e80..83116bc8370 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -5,7 +5,7 @@ trackers and external authentication. See the documentation below for details on how to configure these services. -- [Jira](jira.md) Integrate with the JIRA issue tracker +- [Jira](../project_services/jira.md) Integrate with the JIRA issue tracker - [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. - [LDAP](ldap.md) Set up sign in via LDAP - [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd and Azure diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index 3543a67dd49..e25af546527 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -19,7 +19,7 @@ To enable an external issue tracker you must configure the appropriate **Service Visit the links below for details: - [Redmine](../project_services/redmine.md) -- [Jira](jira.md) +- [Jira](../proect_services/jira.md) ### Service Template diff --git a/doc/integration/img/jira_issue_reference.png b/doc/integration/img/jira_issue_reference.png deleted file mode 100644 index 15739a22dc7..00000000000 Binary files a/doc/integration/img/jira_issue_reference.png and /dev/null differ diff --git a/doc/integration/img/jira_merge_request_close.png b/doc/integration/img/jira_merge_request_close.png deleted file mode 100644 index 1e78daf105f..00000000000 Binary files a/doc/integration/img/jira_merge_request_close.png and /dev/null differ diff --git a/doc/integration/img/jira_project_name.png b/doc/integration/img/jira_project_name.png deleted file mode 100644 index 5986fdb63fb..00000000000 Binary files a/doc/integration/img/jira_project_name.png and /dev/null differ diff --git a/doc/integration/img/jira_service.png b/doc/integration/img/jira_service.png deleted file mode 100644 index 1f6628c4371..00000000000 Binary files a/doc/integration/img/jira_service.png and /dev/null differ diff --git a/doc/integration/img/jira_service_close_issue.png b/doc/integration/img/jira_service_close_issue.png deleted file mode 100644 index 67dfc6144c4..00000000000 Binary files a/doc/integration/img/jira_service_close_issue.png and /dev/null differ diff --git a/doc/integration/img/jira_service_page.png b/doc/integration/img/jira_service_page.png deleted file mode 100644 index 2b37eda3520..00000000000 Binary files a/doc/integration/img/jira_service_page.png and /dev/null differ diff --git a/doc/integration/img/jira_workflow_screenshot.png b/doc/integration/img/jira_workflow_screenshot.png deleted file mode 100644 index 8635a32eb68..00000000000 Binary files a/doc/integration/img/jira_workflow_screenshot.png and /dev/null differ diff --git a/doc/integration/jira.md b/doc/integration/jira.md index de574d53410..78aa6634116 100644 --- a/doc/integration/jira.md +++ b/doc/integration/jira.md @@ -1,149 +1,3 @@ -# GitLab Jira integration +# GitLab JIRA integration -GitLab can be configured to interact with Jira. Configuration happens via -username and password. Connecting to a Jira server via CAS is not possible. - -Each project can be configured to connect to a different Jira instance, see the -[configuration](#configuration) section. If you have one Jira instance you can -pre-fill the settings page with a default template. To configure the template -see the [Services Templates][services-templates] document. - -Once the project is connected to Jira, you can reference and close the issues -in Jira directly from GitLab. - -## Table of Contents - -* [Referencing Jira Issues from GitLab](#referencing-jira-issues) -* [Closing Jira Issues from GitLab](#closing-jira-issues) -* [Configuration](#configuration) - -### Referencing Jira Issues - -When GitLab project has Jira issue tracker configured and enabled, mentioning -Jira issue in GitLab will automatically add a comment in Jira issue with the -link back to GitLab. This means that in comments in merge requests and commits -referencing an issue, eg. `PROJECT-7`, will add a comment in Jira issue in the -format: - -``` - USER mentioned this issue in LINK_TO_THE_MENTION -``` - -* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab. -* `LINK_TO_THE_MENTION` Link to the origin of mention with a name of the entity where Jira issue was mentioned. -Can be commit or merge request. - -![example of mentioning or closing the Jira issue](img/jira_issue_reference.png) - ---- - -### Closing Jira Issues - -Jira issues can be closed directly from GitLab by using trigger words, eg. -`Resolves PROJECT-1`, `Closes PROJECT-1` or `Fixes PROJECT-1`, in commits and -merge requests. When a commit which contains the trigger word in the commit -message is pushed, GitLab will add a comment in the mentioned Jira issue. - -For example, for project named `PROJECT` in Jira, we implemented a new feature -and created a merge request in GitLab. - -This feature was requested in Jira issue `PROJECT-7`. Merge request in GitLab -contains the improvement and in merge request description we say that this -merge request `Closes PROJECT-7` issue. - -Once this merge request is merged, the Jira issue will be automatically closed -with a link to the commit that resolved the issue. - -![A Git commit that causes the Jira issue to be closed](img/jira_merge_request_close.png) - ---- - -![The GitLab integration user leaves a comment on Jira](img/jira_service_close_issue.png) - ---- - -## Configuration - -### Configuring JIRA - -We need to create a user in JIRA which will have access to all projects that -need to integrate with GitLab. Login to your JIRA instance as admin and under -Administration go to User Management and create a new user. - -As an example, we'll create a user named `gitlab` and add it to `jira-developers` -group. - -**It is important that the user `gitlab` has write-access to projects in JIRA** - -### Configuring GitLab - -JIRA configuration in GitLab is done via a project's **Services**. - -#### GitLab 7.8 and up with JIRA v6.x - -See next section. - -#### GitLab 7.8 and up - -_The currently supported JIRA versions are v6.x and v7.x._ - -To enable JIRA integration in a project, navigate to the project's -**Settings > Services > JIRA**. - -Fill in the required details on the page as described in the table below. - -| Field | Description | -| ----- | ----------- | -| `description` | A name for the issue tracker (to differentiate between instances, for instance). | -| `project url` | The URL to the JIRA project which is being linked to this GitLab project. | -| `issues url` | The URL to the JIRA project issues overview for the project that is linked to this GitLab project. | -| `new issue url` | This is the URL to create a new issue in JIRA for the project linked to this GitLab project. | -| `api url` | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`, i.e. `https://jira.example.com/rest/api/2`. | -| `username` | The username of the user created in [configuring JIRA step](#configuring-jira). | -| `password` |The password of the user created in [configuring JIRA step](#configuring-jira). | -| `Jira issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). By default, this ID is `2` (in the example image, this is `2` as well) | - -After saving the configuration, your GitLab project will be able to interact -with the linked JIRA project. - -![Jira service page](img/jira_service_page.png) - ---- - -#### GitLab 6.x-7.7 with JIRA v6.x - -_**Note:** GitLab versions 7.8 and up contain various integration improvements. -We strongly recommend upgrading._ - -In `gitlab.yml` enable the JIRA issue tracker section by -[uncommenting these lines][jira-gitlab-yml]. This will make sure that all -issues within GitLab are pointing to the JIRA issue tracker. - -After you set this, you will be able to close issues in JIRA by a commit in -GitLab. - -Go to your project's **Settings** page and fill in the project name for the -JIRA project: - -![Set the JIRA project name in GitLab to 'NEW'](img/jira_project_name.png) - ---- - -You can also enable the JIRA service that will allow you to interact with JIRA -issues. Go to the **Settings > Services > JIRA** and: - -1. Tick the active check box to enable the service -1. Supply the URL to JIRA server, for example http://jira.example.com -1. Supply the username of a user we created under `Configuring JIRA` section, - for example `gitlab` -1. Supply the password of the user -1. Optional: supply the JIRA API version, default is version `2` -1. Optional: supply the JIRA issue transition ID (issue transition to closed). - This is dependent on JIRA settings, default is `2` -1. Hit save - - -![Jira services page](img/jira_service.png) - -[services-templates]: ../project_services/services_templates.md -[jira-gitlab-yml]: https://gitlab.com/subscribers/gitlab-ee/blob/6-8-stable-ee/config/gitlab.yml.example#L111-115 +This document was moved under [project_services/jira](../project_services/jira.md). diff --git a/doc/project_services/img/jira_issue_reference.png b/doc/project_services/img/jira_issue_reference.png new file mode 100644 index 00000000000..15739a22dc7 Binary files /dev/null and b/doc/project_services/img/jira_issue_reference.png differ diff --git a/doc/project_services/img/jira_merge_request_close.png b/doc/project_services/img/jira_merge_request_close.png new file mode 100644 index 00000000000..1e78daf105f Binary files /dev/null and b/doc/project_services/img/jira_merge_request_close.png differ diff --git a/doc/project_services/img/jira_project_name.png b/doc/project_services/img/jira_project_name.png new file mode 100644 index 00000000000..5986fdb63fb Binary files /dev/null and b/doc/project_services/img/jira_project_name.png differ diff --git a/doc/project_services/img/jira_service.png b/doc/project_services/img/jira_service.png new file mode 100644 index 00000000000..1f6628c4371 Binary files /dev/null and b/doc/project_services/img/jira_service.png differ diff --git a/doc/project_services/img/jira_service_close_issue.png b/doc/project_services/img/jira_service_close_issue.png new file mode 100644 index 00000000000..67dfc6144c4 Binary files /dev/null and b/doc/project_services/img/jira_service_close_issue.png differ diff --git a/doc/project_services/img/jira_service_page.png b/doc/project_services/img/jira_service_page.png new file mode 100644 index 00000000000..2b37eda3520 Binary files /dev/null and b/doc/project_services/img/jira_service_page.png differ diff --git a/doc/project_services/img/jira_workflow_screenshot.png b/doc/project_services/img/jira_workflow_screenshot.png new file mode 100644 index 00000000000..8635a32eb68 Binary files /dev/null and b/doc/project_services/img/jira_workflow_screenshot.png differ diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index e3403127723..f004f3e8789 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -22,7 +22,7 @@ further configuration instructions and details. Contributions are welcome. | Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | | [HipChat](hipchat.md) | Private group chat and IM | | [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | -| JIRA | Jira issue tracker | +| [JIRA](jira.md) | Jira issue tracker | | JetBrains TeamCity CI | A continuous integration and build server | | PivotalTracker | Project Management Software (Source Commits Endpoint) | | Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | -- cgit v1.2.1 From 5637423c698d373232f70af91e13bd16610510ac Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 25 Jan 2016 10:08:44 +0100 Subject: Split JIRA configuration for GitLab < 7.8 and > 7.8 [ci skip] --- doc/project_services/jira.md | 122 +++++++++++++++++++++++++++++++++++++++ doc/project_services/jira_old.md | 40 +++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 doc/project_services/jira.md create mode 100644 doc/project_services/jira_old.md diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md new file mode 100644 index 00000000000..3e0129bfe37 --- /dev/null +++ b/doc/project_services/jira.md @@ -0,0 +1,122 @@ +# GitLab JIRA integration + +GitLab can be configured to interact with JIRA. Configuration happens via +username and password. Connecting to a JIRA server via CAS is not possible. + +Each project can be configured to connect to a different JIRA instance, see the +[configuration](#configuration) section. If you have one JIRA instance you can +pre-fill the settings page with a default template. To configure the template +see the [Services Templates][services-templates] document. + +Once the project is connected to JIRA, you can reference and close the issues +in JIRA directly from GitLab. + +## Configuration + +The configuration consists of two parts: + +- [JIRA configuration](#configuring-jira) +- [GitLab configuration](#configuring-gitlab) + +### Configuring JIRA + +We need to create a user in JIRA which will have access to all projects that +need to integrate with GitLab. Login to your JIRA instance as admin and under +Administration go to User Management and create a new user. + +As an example, we'll create a user named `gitlab` and add it to `jira-developers` +group. + +**It is important that the user `gitlab` has write-access to projects in JIRA** + +### Configuring GitLab + +JIRA configuration in GitLab is done via a project's +[**Services**](../project_services/project_services.md). + +#### GitLab 7.8 and up + +_The currently supported JIRA versions are v6.x and v7.x._ + +To enable JIRA integration in a project, navigate to the project's +**Settings > Services > JIRA**. + +Fill in the required details on the page as described in the table below. + +| Field | Description | +| ----- | ----------- | +| `description` | A name for the issue tracker (to differentiate between instances, for instance). | +| `project url` | The URL to the JIRA project which is being linked to this GitLab project. | +| `issues url` | The URL to the JIRA project issues overview for the project that is linked to this GitLab project. | +| `new issue url` | This is the URL to create a new issue in JIRA for the project linked to this GitLab project. | +| `api url` | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`, i.e. `https://jira.example.com/rest/api/2`. | +| `username` | The username of the user created in [configuring JIRA step](#configuring-jira). | +| `password` |The password of the user created in [configuring JIRA step](#configuring-jira). | +| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). By default, this ID is `2` (in the example image, this is `2` as well) | + +After saving the configuration, your GitLab project will be able to interact +with the linked JIRA project. + +![JIRA service page](img/jira_service_page.png) + +--- + +#### GitLab 6.x-7.7 with JIRA v6.x + +_**Note:** GitLab versions 7.8 and up contain various integration improvements. +We strongly recommend upgrading._ + +In the unfortunate event that you are still using GitLab < 7.8, consult the +[jira_old document](jira_old.md) on how to configure JIRA. + +## JIRA issues + +### Referencing JIRA Issues + +When GitLab project has JIRA issue tracker configured and enabled, mentioning +JIRA issue in GitLab will automatically add a comment in JIRA issue with the +link back to GitLab. This means that in comments in merge requests and commits +referencing an issue, eg. `PROJECT-7`, will add a comment in JIRA issue in the +format: + +``` + USER mentioned this issue in LINK_TO_THE_MENTION +``` + +Where: + +| Format | Description | +| ------ | ----------- | +| `USER` | A user that mentioned the issue. This is the link to the user profile in GitLab. | +| `LINK_TO_THE_MENTION` | Link to the origin of mention with a name of the entity where JIRA issue was mentioned. Can be commit or merge request. | + +![example of mentioning or closing the JIRA issue](img/jira_issue_reference.png) + +--- + +### Closing JIRA Issues + +JIRA issues can be closed directly from GitLab by using trigger words, eg. +`Resolves PROJECT-1`, `Closes PROJECT-1` or `Fixes PROJECT-1`, in commits and +merge requests. When a commit which contains the trigger word in the commit +message is pushed, GitLab will add a comment in the mentioned JIRA issue. + +For example, for project named `PROJECT` in JIRA, we implemented a new feature +and created a merge request in GitLab. + +This feature was requested in JIRA issue `PROJECT-7`. Merge request in GitLab +contains the improvement and in merge request description we say that this +merge request `Closes PROJECT-7` issue. + +Once this merge request is merged, the JIRA issue will be automatically closed +with a link to the commit that resolved the issue. + +![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.png) + +--- + +![The GitLab integration user leaves a comment on JIRA](img/jira_service_close_issue.png) + +--- + +[services-templates]: ../project_services/services_templates.md diff --git a/doc/project_services/jira_old.md b/doc/project_services/jira_old.md new file mode 100644 index 00000000000..2813b142de2 --- /dev/null +++ b/doc/project_services/jira_old.md @@ -0,0 +1,40 @@ +# GitLab 6.x-7.7 with JIRA v6.x + +**NOTE: This method is deprecated. GitLab versions 7.8 and up, contain various +integration improvements and we strongly recommend upgrading. The official +supported document on JIRA integration can be found under [JIRA](jira.md).** + +--- + +In `gitlab.yml` enable the JIRA issue tracker section by +[uncommenting these lines][jira-gitlab-yml]. This will make sure that all +issues within GitLab are pointing to the JIRA issue tracker. + +After you set this, you will be able to close issues in JIRA by a commit in +GitLab. + +Go to your project's **Settings** page and fill in the project name for the +JIRA project: + +![Set the JIRA project name in GitLab to 'NEW'](img/jira_project_name.png) + +--- + +You can also enable the JIRA service that will allow you to interact with JIRA +issues. Go to the **Settings > Services > JIRA** and: + +1. Tick the active check box to enable the service +1. Supply the URL to JIRA server, for example https://jira.example.com +1. Supply the username of a user we created under `Configuring JIRA` section, + for example `gitlab` +1. Supply the password of the user +1. Optional: supply the JIRA API version, default is version `2` +1. Optional: supply the JIRA issue transition ID (issue transition to closed). + This is dependent on JIRA settings, default is `2` +1. Hit save + + +![JIRA services page](img/jira_service.png) + + +[jira-gitlab-yml]: https://gitlab.com/subscribers/gitlab-ee/blob/6-8-stable-ee/config/gitlab.yml.example#L111-115 -- cgit v1.2.1 From 4cfa119d5bd74a323b7a095b527bfeda17b63ef5 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Mon, 25 Jan 2016 11:13:40 +0200 Subject: rails updated to 4.2.5 --- Gemfile | 2 +- Gemfile.lock | 60 ++++++++++++++++++++++++++++++------------------------------ 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/Gemfile b/Gemfile index 8e727d950bf..1d367441364 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source "https://rubygems.org" -gem 'rails', '4.2.4' +gem 'rails', '4.2.5' gem 'rails-deprecated_sanitizer', '~> 1.0.3' # Responders respond_to and respond_with diff --git a/Gemfile.lock b/Gemfile.lock index 5e0718af70f..93069a61a54 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,41 +4,41 @@ GEM CFPropertyList (2.3.2) RedCloth (4.2.9) ace-rails-ap (2.0.1) - actionmailer (4.2.4) - actionpack (= 4.2.4) - actionview (= 4.2.4) - activejob (= 4.2.4) + actionmailer (4.2.5) + actionpack (= 4.2.5) + actionview (= 4.2.5) + activejob (= 4.2.5) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.4) - actionview (= 4.2.4) - activesupport (= 4.2.4) + actionpack (4.2.5) + actionview (= 4.2.5) + activesupport (= 4.2.5) rack (~> 1.6) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.4) - activesupport (= 4.2.4) + actionview (4.2.5) + activesupport (= 4.2.5) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - activejob (4.2.4) - activesupport (= 4.2.4) + activejob (4.2.5) + activesupport (= 4.2.5) globalid (>= 0.3.0) - activemodel (4.2.4) - activesupport (= 4.2.4) + activemodel (4.2.5) + activesupport (= 4.2.5) builder (~> 3.1) - activerecord (4.2.4) - activemodel (= 4.2.4) - activesupport (= 4.2.4) + activerecord (4.2.5) + activemodel (= 4.2.5) + activesupport (= 4.2.5) arel (~> 6.0) activerecord-deprecated_finders (1.0.4) activerecord-session_store (0.1.2) actionpack (>= 4.0.0, < 5) activerecord (>= 4.0.0, < 5) railties (>= 4.0.0, < 5) - activesupport (4.2.4) + activesupport (4.2.5) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) @@ -588,16 +588,16 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) - rails (4.2.4) - actionmailer (= 4.2.4) - actionpack (= 4.2.4) - actionview (= 4.2.4) - activejob (= 4.2.4) - activemodel (= 4.2.4) - activerecord (= 4.2.4) - activesupport (= 4.2.4) + rails (4.2.5) + actionmailer (= 4.2.5) + actionpack (= 4.2.5) + actionview (= 4.2.5) + activejob (= 4.2.5) + activemodel (= 4.2.5) + activerecord (= 4.2.5) + activesupport (= 4.2.5) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.4) + railties (= 4.2.5) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) @@ -607,9 +607,9 @@ GEM rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.2) loofah (~> 2.0) - railties (4.2.4) - actionpack (= 4.2.4) - activesupport (= 4.2.4) + railties (4.2.5) + actionpack (= 4.2.5) + activesupport (= 4.2.5) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.0.0) @@ -988,7 +988,7 @@ DEPENDENCIES rack-attack (~> 4.3.1) rack-cors (~> 0.4.0) rack-oauth2 (~> 1.2.1) - rails (= 4.2.4) + rails (= 4.2.5) rails-deprecated_sanitizer (~> 1.0.3) raphael-rails (~> 2.1.2) rblineprof -- cgit v1.2.1 From 25bde645d8fb0176ccc78e257ac876f667c857b9 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 25 Jan 2016 10:56:56 +0100 Subject: Install unzip in build environment [ci skip] --- scripts/prepare_build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index b5d15847bca..5987988dc8e 100755 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -10,7 +10,7 @@ if [ -f /.dockerinit ]; then apt-get update -qq apt-get -o dir::cache::archives="/cache/apt" install -y -qq --force-yes \ - libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client + libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip cp config/database.yml.mysql config/database.yml sed -i 's/username:.*/username: root/g' config/database.yml -- cgit v1.2.1 From 40d2daed89f598e0b869bdc09bf47d4defe1c5f0 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 25 Jan 2016 11:42:47 +0100 Subject: Highlight note code and edit preview --- app/assets/javascripts/blob/edit_blob.js.coffee | 1 + app/assets/stylesheets/framework/common.scss | 8 -------- app/assets/stylesheets/framework/highlight.scss | 18 ++---------------- app/assets/stylesheets/framework/tables.scss | 4 ++-- app/assets/stylesheets/framework/typography.scss | 18 ++++++------------ app/assets/stylesheets/highlight/dark.scss | 4 ++-- app/assets/stylesheets/highlight/monokai.scss | 4 ++-- app/assets/stylesheets/highlight/solarized_dark.scss | 8 ++++---- app/assets/stylesheets/highlight/solarized_light.scss | 8 ++++---- app/assets/stylesheets/highlight/white.scss | 4 ++-- app/assets/stylesheets/pages/diff.scss | 5 +---- app/assets/stylesheets/pages/notes.scss | 1 + app/views/projects/blob/_text.html.haml | 8 ++++---- app/views/projects/blob/preview.html.haml | 2 +- app/views/projects/notes/discussions/_diff.html.haml | 2 +- app/views/shared/snippets/_blob.html.haml | 3 +-- 16 files changed, 34 insertions(+), 64 deletions(-) diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee index f6bf836f19f..390e41ed8d4 100644 --- a/app/assets/javascripts/blob/edit_blob.js.coffee +++ b/app/assets/javascripts/blob/edit_blob.js.coffee @@ -32,6 +32,7 @@ class @EditBlob content: editor.getValue() , (response) -> currentPane.empty().append response + currentPane.syntaxHighlight() return else diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 9bc814cfd2d..6ea2219073c 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -311,14 +311,6 @@ table { } } -.wiki .highlight, .note-body .highlight { - margin: 12px 0 12px 0; -} - -.wiki .code { - overflow-x: auto; -} - .footer-links { margin-bottom: 20px; a { diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss index 2e13ee842e0..9854df4c45c 100644 --- a/app/assets/stylesheets/framework/highlight.scss +++ b/app/assets/stylesheets/framework/highlight.scss @@ -17,6 +17,7 @@ overflow-y: hidden; white-space: pre; word-wrap: normal; + border-left: 1px solid; code { font-family: $monospace_font; @@ -25,7 +26,7 @@ padding: 0; .line { - display: inline; + display: inline-block; } } } @@ -53,18 +54,3 @@ } } } - -.note-text .code { - border: none; - box-shadow: none; - background: $background-color; - padding: 1em; - overflow-x: auto; - - code { - font-family: $monospace_font; - white-space: pre; - word-wrap: normal; - padding: 0; - } -} diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index c4e9f467ce4..b5134f44ded 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -33,11 +33,11 @@ table { background-color: $background-color; font-weight: normal; font-size: 15px; - border-bottom: 1px solid $border-color !important; + border-bottom: 1px solid $border-color; } td { - border-color: $table-border-color !important; + border-color: $table-border-color; border-bottom: 1px solid; } } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index ab4f71af039..4866a17005d 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -87,8 +87,8 @@ } p { - color:#5c5d5e; - margin:6px 0 0 0; + color: #5c5d5e; + margin: 6px 0 0 0; } table { @@ -102,11 +102,10 @@ } pre { - margin: 12px 0 12px 0 !important; - background-color: #f8fafc; - font-size: 13px !important; - color: #5b6169; - line-height: 1.6em !important; + margin: 12px 0 12px 0; + font-size: 13px; + line-height: 1.6em; + overflow-x: auto; @include border-radius(2px); } @@ -204,11 +203,6 @@ h1, h2, h3, h4, h5, h6 { pre { font-family: $monospace_font; - &.dark { - background: #333; - color: $background-color; - } - &.plain-readme { background: none; border: none; diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index c7f7a5aaf07..41a4579e1bc 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -10,8 +10,8 @@ } // Code itself - pre.code { - border-left: 1px solid #666; + pre.code, .diff-line-num { + border-color: #666; } &, pre.code, .line_holder .line_content { diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index 516d8aef960..22cee1af39f 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -10,8 +10,8 @@ } // Code itself - pre.code { - border-left: 1px solid #555; + pre.code, .diff-line-num { + border-color: #555; } &, pre.code, .line_holder .line_content { diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index b0aaadd0f7f..0e8f30b4b0b 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -10,8 +10,8 @@ } // Code itself - pre.code { - border-left: 1px solid #113b46; + pre.code, .diff-line-num { + border-color: #113b46; } &, pre.code, .line_holder .line_content { @@ -22,11 +22,11 @@ // Diff line .line_holder { .diff-line-num.new, .line_content.new { - @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.3), #808080); + @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.3), #113b46); } .diff-line-num.old, .line_content.old { - @include diff_background(rgba(220, 50, 47, 0.3), rgba(220, 50, 47, 0.3), #808080); + @include diff_background(rgba(220, 50, 47, 0.3), rgba(220, 50, 47, 0.3), #113b46); } .line_content.match { diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index 1c138572145..08b6c835907 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -10,8 +10,8 @@ } // Code itself - pre.code { - border-left: 1px solid #c5d0d4; + pre.code, .diff-line-num { + border-color: #c5d0d4; } &, pre.code, .line_holder .line_content { @@ -22,11 +22,11 @@ // Diff line .line_holder { .diff-line-num.new, .line_content.new { - @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.3), #FAF3DD); + @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.3), #c5d0d4); } .diff-line-num.old, .line_content.old { - @include diff_background(rgba(220, 50, 47, 0.2), rgba(220, 50, 47, 0.3), #FAF3DD); + @include diff_background(rgba(220, 50, 47, 0.2), rgba(220, 50, 47, 0.3), #c5d0d4); } .line_content.match { diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 8a932e6540e..8a091028a6c 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -10,8 +10,8 @@ } // Code itself - pre.code { - border-left: 1px solid $border-color; + pre.code, .diff-line-num { + border-color: $border-color; } &, pre.code, .line_holder .line_content { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 5215df04a6e..a7925e79549 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -79,10 +79,8 @@ margin: 0px; padding: 0px; border: none; - background: $background-color; - color: rgba(0, 0, 0, 0.3); padding: 0px 5px; - border-right: 1px solid $border-color; + border-right: 1px solid; text-align: right; min-width: 35px; max-width: 50px; @@ -92,7 +90,6 @@ float: left; width: 35px; font-weight: normal; - color: rgba(0, 0, 0, 0.3); &:hover { text-decoration: underline; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index f6343e6bb1e..19ead07c06a 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -154,6 +154,7 @@ ul.notes { text-align: center; padding: 10px 0; background: #FFF; + color: $text-color; } &.notes_line2 { text-align: center; diff --git a/app/views/projects/blob/_text.html.haml b/app/views/projects/blob/_text.html.haml index 4429c395aee..906e5ccb360 100644 --- a/app/views/projects/blob/_text.html.haml +++ b/app/views/projects/blob/_text.html.haml @@ -2,8 +2,8 @@ .file-content.wiki = render_markup(blob.name, blob.data) - else - .file-content.code - - unless blob.empty? - = render 'shared/file_highlight', blob: blob - - else + - unless blob.empty? + = render 'shared/file_highlight', blob: blob + - else + .file-content.code .nothing-here-block Empty file diff --git a/app/views/projects/blob/preview.html.haml b/app/views/projects/blob/preview.html.haml index c5a269f334c..541dc96c45f 100644 --- a/app/views/projects/blob/preview.html.haml +++ b/app/views/projects/blob/preview.html.haml @@ -8,7 +8,7 @@ .file-content.wiki = raw render_markup(@blob.name, @content) - else - .file-content.code + .file-content.code.js-syntax-highlight - unless @diff_lines.empty? %table.text-file - @diff_lines.each do |line| diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml index 92bbaf0961a..820e31ccd61 100644 --- a/app/views/projects/notes/discussions/_diff.html.haml +++ b/app/views/projects/notes/discussions/_diff.html.haml @@ -9,7 +9,7 @@ = diff.new_path - if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode %span.file-mode= "#{diff.a_mode} → #{diff.b_mode}" - .diff-content + .diff-content.code.js-syntax-highlight %table - note.truncated_diff_lines.each do |line| - type = line.type diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml index d26a99bb14c..e0e41fc4bea 100644 --- a/app/views/shared/snippets/_blob.html.haml +++ b/app/views/shared/snippets/_blob.html.haml @@ -3,8 +3,7 @@ .file-content.wiki = render_markup(@snippet.file_name, @snippet.data) - else - .file-content.code - = render 'shared/file_highlight', blob: @snippet + = render 'shared/file_highlight', blob: @snippet - else .file-content.code .nothing-here-block Empty file -- cgit v1.2.1 From ee209cc936d9242af2b2d8d45ecb64adc55e3a51 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 25 Jan 2016 11:44:25 +0100 Subject: fix rubocop new 2.2 syntax issues --- app/helpers/commits_helper.rb | 2 +- app/helpers/projects_helper.rb | 2 +- spec/models/concerns/case_sensitivity_spec.rb | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index d26f007c8e6..53f8f913b33 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -152,7 +152,7 @@ module CommitsHelper options = { class: "commit-#{options[:source]}-link has_tooltip", - data: { :'original-title' => sanitize(source_email) } + data: { 'original-title': sanitize(source_email) } } if user.nil? diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 77ba612548a..c5823e50096 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -40,7 +40,7 @@ module ProjectsHelper link_to(author_html, user_path(author), class: "author_link").html_safe else title = opts[:title].sub(":name", sanitize(author.name)) - link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { :'original-title' => title, container: 'body' } ).html_safe + link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { 'original-title': title, container: 'body' } ).html_safe end end diff --git a/spec/models/concerns/case_sensitivity_spec.rb b/spec/models/concerns/case_sensitivity_spec.rb index 25b3f4e50da..6535246bf2d 100644 --- a/spec/models/concerns/case_sensitivity_spec.rb +++ b/spec/models/concerns/case_sensitivity_spec.rb @@ -37,7 +37,7 @@ describe CaseSensitivity, models: true do with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar'). and_return(criteria) - expect(model.iwhere(:'foo.bar' => 'bar')).to eq(criteria) + expect(model.iwhere('foo.bar': 'bar')).to eq(criteria) end end @@ -87,8 +87,8 @@ describe CaseSensitivity, models: true do with(%q{LOWER("foo"."baz") = LOWER(:value)}, value: 'baz'). and_return(final) - got = model.iwhere(:'foo.bar' => 'bar', - :'foo.baz' => 'baz') + got = model.iwhere('foo.bar': 'bar', + 'foo.baz': 'baz') expect(got).to eq(final) end @@ -127,7 +127,7 @@ describe CaseSensitivity, models: true do with(%q{`foo`.`bar` = :value}, value: 'bar'). and_return(criteria) - expect(model.iwhere(:'foo.bar' => 'bar')). + expect(model.iwhere('foo.bar': 'bar')). to eq(criteria) end end @@ -178,8 +178,8 @@ describe CaseSensitivity, models: true do with(%q{`foo`.`baz` = :value}, value: 'baz'). and_return(final) - got = model.iwhere(:'foo.bar' => 'bar', - :'foo.baz' => 'baz') + got = model.iwhere('foo.bar': 'bar', + 'foo.baz': 'baz') expect(got).to eq(final) end -- cgit v1.2.1 From 870e38ead93901bbe5a2a809422e60d555cbf8e8 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 25 Jan 2016 12:39:50 +0100 Subject: updated docs to reflect version update [ci skip] --- README.md | 2 +- doc/install/installation.md | 2 +- doc/install/requirements.md | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3ec1d4a776c..6ab3b1fcc5f 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Instructions on how to start GitLab and how to run the tests can be found in the GitLab is a Ruby on Rails application that runs on the following software: - Ubuntu/Debian/CentOS/RHEL -- Ruby (MRI) 2.1 +- Ruby (MRI) 2.2 - Git 1.7.10+ - Redis 2.8+ - MySQL or PostgreSQL diff --git a/doc/install/installation.md b/doc/install/installation.md index aef35436571..1c0da6fbef4 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -107,7 +107,7 @@ Then select 'Internet Site' and press enter to confirm the hostname. ## 2. Ruby -_**Note:** The current supported Ruby versions are 2.1.x. Ruby 2.2 and 2.3 are +_**Note:** The current supported Ruby version is 2.2.x. Ruby 2.3 is currently not supported._ The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab diff --git a/doc/install/requirements.md b/doc/install/requirements.md index c0425f27ab1..fde1aaa90f3 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -32,8 +32,7 @@ Please consider using a virtual machine to run GitLab. ## Ruby versions -GitLab requires Ruby (MRI) 2.1.x and currently does not work with versions 2.2 -and 2.3. +GitLab requires Ruby (MRI) 2.2.x and currently does not work with version 2.3. You will have to use the standard MRI implementation of Ruby. We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab -- cgit v1.2.1 From d5fc2e9ded87579c588972da5e53f635e4f1d36d Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 25 Jan 2016 13:48:36 +0100 Subject: Add steps on configuring JIRA [ci skip] --- .../img/jira_add_user_to_group.png | Bin 0 -> 59251 bytes doc/project_services/img/jira_create_new_group.png | Bin 0 -> 41294 bytes .../img/jira_create_new_group_name.png | Bin 0 -> 12535 bytes doc/project_services/img/jira_create_new_user.png | Bin 0 -> 26532 bytes doc/project_services/img/jira_group_access.png | Bin 0 -> 46028 bytes .../img/jira_user_management_link.png | Bin 0 -> 58211 bytes doc/project_services/jira.md | 75 +++++++++++++++++---- 7 files changed, 62 insertions(+), 13 deletions(-) create mode 100644 doc/project_services/img/jira_add_user_to_group.png create mode 100644 doc/project_services/img/jira_create_new_group.png create mode 100644 doc/project_services/img/jira_create_new_group_name.png create mode 100644 doc/project_services/img/jira_create_new_user.png create mode 100644 doc/project_services/img/jira_group_access.png create mode 100644 doc/project_services/img/jira_user_management_link.png diff --git a/doc/project_services/img/jira_add_user_to_group.png b/doc/project_services/img/jira_add_user_to_group.png new file mode 100644 index 00000000000..e4576433889 Binary files /dev/null and b/doc/project_services/img/jira_add_user_to_group.png differ diff --git a/doc/project_services/img/jira_create_new_group.png b/doc/project_services/img/jira_create_new_group.png new file mode 100644 index 00000000000..edaa1326058 Binary files /dev/null and b/doc/project_services/img/jira_create_new_group.png differ diff --git a/doc/project_services/img/jira_create_new_group_name.png b/doc/project_services/img/jira_create_new_group_name.png new file mode 100644 index 00000000000..9e518ad7843 Binary files /dev/null and b/doc/project_services/img/jira_create_new_group_name.png differ diff --git a/doc/project_services/img/jira_create_new_user.png b/doc/project_services/img/jira_create_new_user.png new file mode 100644 index 00000000000..57e433dd818 Binary files /dev/null and b/doc/project_services/img/jira_create_new_user.png differ diff --git a/doc/project_services/img/jira_group_access.png b/doc/project_services/img/jira_group_access.png new file mode 100644 index 00000000000..47716ca6d0e Binary files /dev/null and b/doc/project_services/img/jira_group_access.png differ diff --git a/doc/project_services/img/jira_user_management_link.png b/doc/project_services/img/jira_user_management_link.png new file mode 100644 index 00000000000..2745916972c Binary files /dev/null and b/doc/project_services/img/jira_user_management_link.png differ diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index 3e0129bfe37..65d850934bb 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -1,15 +1,15 @@ # GitLab JIRA integration -GitLab can be configured to interact with JIRA. Configuration happens via +GitLab can be configured to interact with [JIRA]. Configuration happens via username and password. Connecting to a JIRA server via CAS is not possible. -Each project can be configured to connect to a different JIRA instance, see the -[configuration](#configuration) section. If you have one JIRA instance you can -pre-fill the settings page with a default template. To configure the template -see the [Services Templates][services-templates] document. +Each project can be configured to connect to a different JIRA instance or, in +case you have one JIRA instance, you can pre-fill the JIRA service settings page +with a default template. To configure the template, see the +[Services Templates documentation][services-templates]. Once the project is connected to JIRA, you can reference and close the issues -in JIRA directly from GitLab. +in JIRA directly from GitLab's Merge requests. ## Configuration @@ -20,14 +20,62 @@ The configuration consists of two parts: ### Configuring JIRA -We need to create a user in JIRA which will have access to all projects that -need to integrate with GitLab. Login to your JIRA instance as admin and under -Administration go to User Management and create a new user. +First things first, we need to create a user in JIRA which will have access to +all projects that need to integrate with GitLab. -As an example, we'll create a user named `gitlab` and add it to `jira-developers` -group. +We have split this stage in steps so it could be easier to follow. -**It is important that the user `gitlab` has write-access to projects in JIRA** +--- + +1. Login to your JIRA instance as an administrator and under **Administration** + go to **User Management** and create a new user. + + ![JIRA user management link](img/jira_user_management_link.png) + + --- + +1. The next step is to create a new user (e.g., `gitlab`) who has write-access + to projects in JIRA. Enter the user's name and a valid e-mail address in + order to set-up their password. + _**Note:** JIRA creates the username automatically by using the e-mail + prefix. You can change the username later if you want._ + + ![JIRA create new user](img/jira_create_new_user.png) + + --- + +1. Now, let's create a `gitlab-developers` group which will have write-access + to projects in JIRA. Go to the **Groups** tab and select **Create group**. + + ![JIRA create new user](img/jira_create_new_group.png) + + --- + + Give it an optional description and hit **Create group**. + + ![JIRA create new group](img/jira_create_new_group_name.png) + + --- + +1. Give the newly-created group write access by going to + **Application access > View configuration** and adding the `gitlab-developers` + group to JIRA Core. + + ![JIRA group access](img/jira_group_access.png) + + --- + +1. Add the `gitlab` user to `gitlab-developers` group by going to + **Users > GitLab user > Add group** and selecting the `gitlab-developers` + group from the dropdown menu. Notice that the group says _Access_ which is + what we aim for. + + ![JIRA add user to group](img/jira_add_user_to_group.png) + +--- + +The JIRA configuration is over. Note the new user `gitlab` and its password as +they will be needed when configuring GitLab in the next section. ### Configuring GitLab @@ -119,4 +167,5 @@ with a link to the commit that resolved the issue. --- -[services-templates]: ../project_services/services_templates.md +[services-templates]: ../project_services/services_templates.md "Services templates documentation" +[JIRA]: https://www.atlassian.com/software/jira/core "The JIRA Core website" -- cgit v1.2.1 From 468dbfe937b60cc0b06f062fd01dcfd70450bcf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Zaj=C4=85czkowski?= Date: Mon, 25 Jan 2016 13:06:09 +0000 Subject: Update Slack integration configuration --- doc/integration/slack.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/doc/integration/slack.md b/doc/integration/slack.md index 84f1d74c058..ecbe0d3e887 100644 --- a/doc/integration/slack.md +++ b/doc/integration/slack.md @@ -6,15 +6,17 @@ To enable Slack integration you must create an Incoming WebHooks integration on 1. [Sign in to Slack](https://slack.com/signin) -1. Select **Configure Integrations** from the dropdown next to your team name. +1. Select **Apps & Custom Integrations** from the dropdown next to your team name. -1. Select the **All Services** tab +1. Click the **Configure** link (right-upper corner). -1. Click **Add** next to Incoming Webhooks +1. Select the **Custom integrations** tab. -1. Pick Incoming WebHooks +1. Click the **Incoming WebHooks** row. -1. Choose the channel name you want to send notifications to +1. Click the **Add configuration** button. + +1. Choose the channel name you want to send notifications to. 1. Click **Add Incoming WebHooks Integration** - Optional step; You can change bot's name and avatar by clicking modifying the bot name or avatar under **Integration Settings**. -- cgit v1.2.1 From 1aa82dc4597d5fcdaa0839f804dd688e641cf85c Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Wed, 20 Jan 2016 22:03:20 -0200 Subject: Prioritize previewable over plain README files --- app/models/tree.rb | 16 +++++++++--- spec/models/tree_spec.rb | 64 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 spec/models/tree_spec.rb diff --git a/app/models/tree.rb b/app/models/tree.rb index e0e04d8859f..b28f31cdd6e 100644 --- a/app/models/tree.rb +++ b/app/models/tree.rb @@ -17,12 +17,20 @@ class Tree def readme return @readme if defined?(@readme) - # Take the first previewable readme, or return nil if none is available or - # we can't preview any of them - readme_tree = blobs.find do |blob| - blob.readme? && (previewable?(blob.name) || plain?(blob.name)) + available_readmes = blobs.select(&:readme?) + + previewable_readmes = available_readmes.select do |blob| + previewable?(blob.name) + end + + plain_readmes = available_readmes.select do |blob| + plain?(blob.name) end + # Prioritize previewable over plain readmes + readme_tree = previewable_readmes.first || plain_readmes.first + + # Return if we can't preview any of them if readme_tree.nil? return @readme = nil end diff --git a/spec/models/tree_spec.rb b/spec/models/tree_spec.rb new file mode 100644 index 00000000000..0737999e125 --- /dev/null +++ b/spec/models/tree_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe Tree, models: true do + let(:repository) { create(:project).repository } + let(:sha) { repository.root_ref } + + subject { described_class.new(repository, '54fcc214') } + + describe '#readme' do + class FakeBlob + attr_reader :name + + def initialize(name) + @name = name + end + + def readme? + name =~ /^readme/i + end + end + + it 'returns nil when repository does not contains a README file' do + files = [FakeBlob.new('file'), FakeBlob.new('license'), FakeBlob.new('copying')] + expect(subject).to receive(:blobs).and_return(files) + + expect(subject.readme).to eq nil + end + + it 'returns nil when repository does not contains a previewable README file' do + files = [FakeBlob.new('file'), FakeBlob.new('README.pages'), FakeBlob.new('README.png')] + expect(subject).to receive(:blobs).and_return(files) + + expect(subject.readme).to eq nil + end + + it 'returns README when repository contains a previewable README file' do + files = [FakeBlob.new('README.png'), FakeBlob.new('README'), FakeBlob.new('file')] + expect(subject).to receive(:blobs).and_return(files) + + expect(subject.readme.name).to eq 'README' + end + + it 'returns first previewable README when repository contains more than one' do + files = [FakeBlob.new('file'), FakeBlob.new('README.md'), FakeBlob.new('README.asciidoc')] + expect(subject).to receive(:blobs).and_return(files) + + expect(subject.readme.name).to eq 'README.md' + end + + it 'returns first plain text README when repository contains more than one' do + files = [FakeBlob.new('file'), FakeBlob.new('README'), FakeBlob.new('README.txt')] + expect(subject).to receive(:blobs).and_return(files) + + expect(subject.readme.name).to eq 'README' + end + + it 'prioritizes previewable README file over one in plain text' do + files = [FakeBlob.new('file'), FakeBlob.new('README'), FakeBlob.new('README.md')] + expect(subject).to receive(:blobs).and_return(files) + + expect(subject.readme.name).to eq 'README.md' + end + end +end -- cgit v1.2.1 From 446fed4d83c4d0a6f1f7bd1930d884636eda0be5 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Mon, 25 Jan 2016 16:52:00 +0200 Subject: monkey patch for mysql 5.7 --- config/initializers/monkey_patch.rb | 48 +++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 config/initializers/monkey_patch.rb diff --git a/config/initializers/monkey_patch.rb b/config/initializers/monkey_patch.rb new file mode 100644 index 00000000000..62b05a55285 --- /dev/null +++ b/config/initializers/monkey_patch.rb @@ -0,0 +1,48 @@ +## This patch is from rails 4.2-stable. Remove it when 4.2.6 is released +## https://github.com/rails/rails/issues/21108 + +module ActiveRecord + module ConnectionAdapters + class AbstractMysqlAdapter < AbstractAdapter + # SHOW VARIABLES LIKE 'name' + def show_variable(name) + variables = select_all("select @@#{name} as 'Value'", 'SCHEMA') + variables.first['Value'] unless variables.empty? + rescue ActiveRecord::StatementInvalid + nil + end + + + # MySQL is too stupid to create a temporary table for use subquery, so we have + # to give it some prompting in the form of a subsubquery. Ugh! + def subquery_for(key, select) + subsubselect = select.clone + subsubselect.projections = [key] + + subselect = Arel::SelectManager.new(select.engine) + subselect.project Arel.sql(key.name) + # Materialized subquery by adding distinct + # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' + subselect.from subsubselect.distinct.as('__active_record_temp') + end + end + end +end + +module ActiveRecord + module ConnectionAdapters + class MysqlAdapter < AbstractMysqlAdapter + ADAPTER_NAME = 'MySQL'.freeze + + # Get the client encoding for this database + def client_encoding + return @client_encoding if @client_encoding + + result = exec_query( + "select @@character_set_client", + 'SCHEMA') + @client_encoding = ENCODINGS[result.rows.last.last] + end + end + end +end -- cgit v1.2.1 From cd22da3ebda969779c47f1652f3876daa5edd7f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 25 Jan 2016 20:00:59 +0100 Subject: Fix preventing migration from crashing in very specific cases See https://gitlab.com/gitlab-org/gitlab-ce/issues/12606 for details --- lib/gitlab/current_settings.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index ea054255820..429f1f9bb56 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -39,6 +39,13 @@ module Gitlab end use_db && ActiveRecord::Base.connection.active? && + # The following condition is important: if a migrations adds a + # column to the application_settings table and a validation in + # the ApplicationSetting uses this new column we might end-up in + # a vicious circle where migration crash before being done. + # See https://gitlab.com/gitlab-org/gitlab-ce/issues/12606 for + # a thorough explanation. + !ActiveRecord::Migrator.needs_migration? && ActiveRecord::Base.connection.table_exists?('application_settings') rescue ActiveRecord::NoDatabaseError -- cgit v1.2.1 From 26e31f5820ff2ccfcf9f84ffe3bdf1d274d13687 Mon Sep 17 00:00:00 2001 From: Jose Torres Date: Wed, 30 Dec 2015 11:13:48 -0600 Subject: Substituted deprecated forum link with project issues link. [ci skip] --- app/views/shared/_promo.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/shared/_promo.html.haml b/app/views/shared/_promo.html.haml index 3596aabe309..09edf4000d5 100644 --- a/app/views/shared/_promo.html.haml +++ b/app/views/shared/_promo.html.haml @@ -1,5 +1,5 @@ .gitlab-promo = link_to 'Homepage', promo_url - = link_to "Blog", promo_url + '/blog/' - = link_to "@gitlab", "https://twitter.com/gitlab" - = link_to "Requests", "http://feedback.gitlab.com/" + = link_to 'Blog', promo_url + '/blog/' + = link_to '@gitlab', 'https://twitter.com/gitlab' + = link_to 'Requests', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#feature-proposals' -- cgit v1.2.1 From 0fcf3adabb84bf62b374916615cde276d2c61843 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 25 Jan 2016 20:29:52 +0100 Subject: JIRA doc clean-up [ci skip] - Add GitLab configuration steps - Add example workflow - Replace old images with new ones --- .../img/jira_add_gitlab_commit_message.png | Bin 0 -> 57136 bytes doc/project_services/img/jira_issues_workflow.png | Bin 0 -> 104791 bytes ...jira_reference_commit_message_in_jira_issue.png | Bin 0 -> 42452 bytes .../img/jira_submit_gitlab_merge_request.png | Bin 0 -> 63063 bytes doc/project_services/jira.md | 144 ++++++++++++++------- 5 files changed, 94 insertions(+), 50 deletions(-) create mode 100644 doc/project_services/img/jira_add_gitlab_commit_message.png create mode 100644 doc/project_services/img/jira_issues_workflow.png create mode 100644 doc/project_services/img/jira_reference_commit_message_in_jira_issue.png create mode 100644 doc/project_services/img/jira_submit_gitlab_merge_request.png diff --git a/doc/project_services/img/jira_add_gitlab_commit_message.png b/doc/project_services/img/jira_add_gitlab_commit_message.png new file mode 100644 index 00000000000..85e54861b3e Binary files /dev/null and b/doc/project_services/img/jira_add_gitlab_commit_message.png differ diff --git a/doc/project_services/img/jira_issues_workflow.png b/doc/project_services/img/jira_issues_workflow.png new file mode 100644 index 00000000000..51a1bc10210 Binary files /dev/null and b/doc/project_services/img/jira_issues_workflow.png differ diff --git a/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png b/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png new file mode 100644 index 00000000000..0149181dc86 Binary files /dev/null and b/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png differ diff --git a/doc/project_services/img/jira_submit_gitlab_merge_request.png b/doc/project_services/img/jira_submit_gitlab_merge_request.png new file mode 100644 index 00000000000..e935d9362aa Binary files /dev/null and b/doc/project_services/img/jira_submit_gitlab_merge_request.png differ diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index 65d850934bb..564a3cc9336 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -1,15 +1,17 @@ # GitLab JIRA integration -GitLab can be configured to interact with [JIRA]. Configuration happens via -username and password. Connecting to a JIRA server via CAS is not possible. +GitLab can be configured to interact with [JIRA Core] either using an +on-premises instance or the SaaS solution that Atlassian offers. Configuration +happens via username and password on a per-project basis. Connecting to a JIRA +server via CAS is not possible. Each project can be configured to connect to a different JIRA instance or, in -case you have one JIRA instance, you can pre-fill the JIRA service settings page -with a default template. To configure the template, see the -[Services Templates documentation][services-templates]. +case you have a single JIRA instance, you can pre-fill the JIRA service +settings page in GitLab with a default template. To configure the JIRA template, +see the [Services Templates documentation][services-templates]. -Once the project is connected to JIRA, you can reference and close the issues -in JIRA directly from GitLab's Merge requests. +Once the GitLab project is connected to JIRA, you can reference and close the +issues in JIRA directly from GitLab's Merge requests. ## Configuration @@ -28,17 +30,17 @@ We have split this stage in steps so it could be easier to follow. --- 1. Login to your JIRA instance as an administrator and under **Administration** - go to **User Management** and create a new user. + go to **User Management** to create a new user. ![JIRA user management link](img/jira_user_management_link.png) --- 1. The next step is to create a new user (e.g., `gitlab`) who has write-access - to projects in JIRA. Enter the user's name and a valid e-mail address in - order to set-up their password. + to projects in JIRA. Enter the user's name and a _valid_ e-mail address + since JIRA sends a verification e-mail to set-up the password. _**Note:** JIRA creates the username automatically by using the e-mail - prefix. You can change the username later if you want._ + prefix. You can change it later if you want._ ![JIRA create new user](img/jira_create_new_user.png) @@ -74,33 +76,36 @@ We have split this stage in steps so it could be easier to follow. --- -The JIRA configuration is over. Note the new user `gitlab` and its password as -they will be needed when configuring GitLab in the next section. +The JIRA configuration is over. Write down the new JIRA username and its +password as they will be needed when configuring GitLab in the next section. -### Configuring GitLab +## Configuring GitLab + +Assuming you [have already configured JIRA](#configuring-jira), now it's time +to configure GitLab. JIRA configuration in GitLab is done via a project's [**Services**](../project_services/project_services.md). #### GitLab 7.8 and up -_The currently supported JIRA versions are v6.x and v7.x._ +_**Note:** The currently supported JIRA versions are v6.x and v7.x._ To enable JIRA integration in a project, navigate to the project's **Settings > Services > JIRA**. -Fill in the required details on the page as described in the table below. +Fill in the required details on the page, as described in the table below. -| Field | Description | -| ----- | ----------- | -| `description` | A name for the issue tracker (to differentiate between instances, for instance). | -| `project url` | The URL to the JIRA project which is being linked to this GitLab project. | -| `issues url` | The URL to the JIRA project issues overview for the project that is linked to this GitLab project. | -| `new issue url` | This is the URL to create a new issue in JIRA for the project linked to this GitLab project. | -| `api url` | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`, i.e. `https://jira.example.com/rest/api/2`. | -| `username` | The username of the user created in [configuring JIRA step](#configuring-jira). | -| `password` |The password of the user created in [configuring JIRA step](#configuring-jira). | -| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). By default, this ID is `2` (in the example image, this is `2` as well) | +| Setting | Description | +| ------- | ----------- | +| `Description` | A name for the issue tracker (to differentiate between instances, for example). | +| `Project url` | The URL to the JIRA project which is being linked to this GitLab project. It's of the form: `https:///issues/?jql=project=`. | +| `Issues url` | The URL to the JIRA project issues overview for the project that is linked to this GitLab project. It is of the form: `https:///browse/:id`. Leave `:id` as-is, it gets replaced by GitLab at runtime. | +| `New issue url` | This is the URL to create a new issue in JIRA for the project linked to this GitLab project, and is of the form: `https:///secure/CreateIssue.jspa` | +| `Api url` | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https:///rest/api/2`. | +| `Username` | The username of the user created in [configuring JIRA step](#configuring-jira). | +| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | +| `JIRA issue transition` | This setting is very important to set up correctly. It is the ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot](img/jira_issues_workflow.png)). By default, this ID is set to `2` | After saving the configuration, your GitLab project will be able to interact with the linked JIRA project. @@ -119,17 +124,38 @@ In the unfortunate event that you are still using GitLab < 7.8, consult the ## JIRA issues +By now you should have [configured JIRA](#configuring-jira) and enabled the +[JIRA service in GitLab](#configuring-gitlab). If everything is set up correctly +you should be able to: + +- reference JIRA issues and +- close JIRA issues + +by just mentioning their ID in GitLab commits and merge requests. + ### Referencing JIRA Issues -When GitLab project has JIRA issue tracker configured and enabled, mentioning -JIRA issue in GitLab will automatically add a comment in JIRA issue with the -link back to GitLab. This means that in comments in merge requests and commits -referencing an issue, eg. `PROJECT-7`, will add a comment in JIRA issue in the -format: +If you reference a JIRA issue, e.g., `GITLAB-1`, in a commit comment, a link +which points back to JIRA is created. + +The same works for comments in merge requests as well. + +![JIRA add GitLab commit message](img/jira_add_gitlab_commit_message.png) + +--- + +The mentioning action is two-fold, so a comment with a JIRA issue in GitLab +will automatically add a comment in that particular JIRA issue with the link +back to GitLab. + + +![JIRA reference commit message](img/jira_reference_commit_message_in_jira_issue.png) + +--- + +The comment on the JIRA issue is of the form: -``` - USER mentioned this issue in LINK_TO_THE_MENTION -``` +> USER mentioned this issue in LINK_TO_THE_MENTION Where: @@ -138,34 +164,52 @@ Where: | `USER` | A user that mentioned the issue. This is the link to the user profile in GitLab. | | `LINK_TO_THE_MENTION` | Link to the origin of mention with a name of the entity where JIRA issue was mentioned. Can be commit or merge request. | -![example of mentioning or closing the JIRA issue](img/jira_issue_reference.png) +### Closing JIRA issues ---- +JIRA issues can be closed directly from GitLab by using trigger words in +commits and merge requests. When a commit, which contains the trigger word +followed by the JIRA issue ID in the commit message, is pushed, GitLab will +add a comment in the mentioned JIRA issue and immediately close it. -### Closing JIRA Issues +There are currently three trigger words, and you can use either one to achieve +the same goal: -JIRA issues can be closed directly from GitLab by using trigger words, eg. -`Resolves PROJECT-1`, `Closes PROJECT-1` or `Fixes PROJECT-1`, in commits and -merge requests. When a commit which contains the trigger word in the commit -message is pushed, GitLab will add a comment in the mentioned JIRA issue. +- `Resolves GITLAB-1` +- `Closes GITLAB-1` +- `Fixes GITLAB-1` -For example, for project named `PROJECT` in JIRA, we implemented a new feature -and created a merge request in GitLab. +where `GITLAB-1` the issue ID of the JIRA project. -This feature was requested in JIRA issue `PROJECT-7`. Merge request in GitLab -contains the improvement and in merge request description we say that this -merge request `Closes PROJECT-7` issue. +### JIRA issue closing example -Once this merge request is merged, the JIRA issue will be automatically closed -with a link to the commit that resolved the issue. +Let's say for example that we submitted a bug fix and created a merge request +in GitLab. The workflow would be something like this: + +1. Create a new branch +1. Fix the bug +1. Commit the changes and push back to GitLab +1. Open a new merge request and reference the JIRA issue including one of the + trigger words, e.g.: `Fixes GITLAB-1`, in the description +1. Submit the merge request +1. Ask someone to review +1. Merge the merge request +1. The JIRA issue is automatically closed -![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.png) +--- + +In the following screenshot you can see how the link references to the JIRA +issue look like. + +![JIRA - submit a GitLab merge request](img/jira_submit_gitlab_merge_request.png) --- +Once this merge request is merged, the JIRA issue will be automatically closed +with a link to the commit that resolved the issue. + ![The GitLab integration user leaves a comment on JIRA](img/jira_service_close_issue.png) --- [services-templates]: ../project_services/services_templates.md "Services templates documentation" -[JIRA]: https://www.atlassian.com/software/jira/core "The JIRA Core website" +[JIRA Core]: https://www.atlassian.com/software/jira/core "The JIRA Core website" -- cgit v1.2.1 From 6c46b79d9a959a9dc74748f6cb3d30fa5ff89264 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 25 Jan 2016 20:36:12 +0100 Subject: Fix sub-heading [ci skip] --- doc/project_services/jira.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index 564a3cc9336..2ade54aeaf8 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -79,7 +79,7 @@ We have split this stage in steps so it could be easier to follow. The JIRA configuration is over. Write down the new JIRA username and its password as they will be needed when configuring GitLab in the next section. -## Configuring GitLab +### Configuring GitLab Assuming you [have already configured JIRA](#configuring-jira), now it's time to configure GitLab. -- cgit v1.2.1 From b74308c0a74ce9256bfe906a070b8751d2cc9e9e Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 25 Jan 2016 21:26:49 +0100 Subject: Correct arity for instrumented methods w/o args This ensures that an instrumented method that doesn't take arguments reports an arity of 0, instead of -1. If Ruby had a proper method for finding out the required arguments of a method (e.g. Method#required_arguments) this would not have been an issue. Sadly the only two methods we have are Method#parameters and Method#arity, and both are equally painful to use. Fixes gitlab-org/gitlab-ce#12450 --- lib/gitlab/metrics/instrumentation.rb | 22 +++++++++++++++++++--- spec/lib/gitlab/metrics/instrumentation_spec.rb | 10 ++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb index d9fce2e6758..face1921d2e 100644 --- a/lib/gitlab/metrics/instrumentation.rb +++ b/lib/gitlab/metrics/instrumentation.rb @@ -106,20 +106,36 @@ module Gitlab if type == :instance target = mod label = "#{mod.name}##{name}" + method = mod.instance_method(name) else target = mod.singleton_class label = "#{mod.name}.#{name}" + method = mod.method(name) + end + + # Some code out there (e.g. the "state_machine" Gem) checks the arity of + # a method to make sure it only passes arguments when the method expects + # any. If we were to always overwrite a method to take an `*args` + # signature this would break things. As a result we'll make sure the + # generated method _only_ accepts regular arguments if the underlying + # method also accepts them. + if method.arity == 0 + args_signature = '&block' + else + args_signature = '*args, &block' end + send_signature = "__send__(#{alias_name.inspect}, #{args_signature})" + target.class_eval <<-EOF, __FILE__, __LINE__ + 1 alias_method #{alias_name.inspect}, #{name.inspect} - def #{name}(*args, &block) + def #{name}(#{args_signature}) trans = Gitlab::Metrics::Instrumentation.transaction if trans start = Time.now - retval = __send__(#{alias_name.inspect}, *args, &block) + retval = #{send_signature} duration = (Time.now - start) * 1000.0 if duration >= Gitlab::Metrics.method_call_threshold @@ -132,7 +148,7 @@ module Gitlab retval else - __send__(#{alias_name.inspect}, *args, &block) + #{send_signature} end end EOF diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb index 2a37cd40dde..ad4290c43bb 100644 --- a/spec/lib/gitlab/metrics/instrumentation_spec.rb +++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb @@ -66,6 +66,16 @@ describe Gitlab::Metrics::Instrumentation do @dummy.foo end + + it 'generates a method with the correct arity when using methods without arguments' do + dummy = Class.new do + def self.test; end + end + + described_class.instrument_method(dummy, :test) + + expect(dummy.method(:test).arity).to eq(0) + end end describe 'with metrics disabled' do -- cgit v1.2.1 From c53aad315f89e0eaa44ab680d0b79f38c4590083 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 25 Jan 2016 22:17:41 +0100 Subject: Add proper screenshot and comment on the output [ci skip] --- doc/project_services/img/jira_issue_closed.png | Bin 0 -> 92601 bytes doc/project_services/img/jira_issues_workflow.png | Bin 104791 -> 105237 bytes doc/project_services/jira.md | 19 ++++++++++++++----- 3 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 doc/project_services/img/jira_issue_closed.png diff --git a/doc/project_services/img/jira_issue_closed.png b/doc/project_services/img/jira_issue_closed.png new file mode 100644 index 00000000000..cabec1ae137 Binary files /dev/null and b/doc/project_services/img/jira_issue_closed.png differ diff --git a/doc/project_services/img/jira_issues_workflow.png b/doc/project_services/img/jira_issues_workflow.png index 51a1bc10210..28e17be3a84 100644 Binary files a/doc/project_services/img/jira_issues_workflow.png and b/doc/project_services/img/jira_issues_workflow.png differ diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index 2ade54aeaf8..b82e4857814 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -167,9 +167,10 @@ Where: ### Closing JIRA issues JIRA issues can be closed directly from GitLab by using trigger words in -commits and merge requests. When a commit, which contains the trigger word -followed by the JIRA issue ID in the commit message, is pushed, GitLab will -add a comment in the mentioned JIRA issue and immediately close it. +commits and merge requests. When a commit which contains the trigger word +followed by the JIRA issue ID in the commit message is pushed, GitLab will +add a comment in the mentioned JIRA issue and immediately close it (provided +the transition ID was set up correctly). There are currently three trigger words, and you can use either one to achieve the same goal: @@ -187,7 +188,7 @@ in GitLab. The workflow would be something like this: 1. Create a new branch 1. Fix the bug -1. Commit the changes and push back to GitLab +1. Commit the changes and push branch to GitLab 1. Open a new merge request and reference the JIRA issue including one of the trigger words, e.g.: `Fixes GITLAB-1`, in the description 1. Submit the merge request @@ -207,9 +208,17 @@ issue look like. Once this merge request is merged, the JIRA issue will be automatically closed with a link to the commit that resolved the issue. -![The GitLab integration user leaves a comment on JIRA](img/jira_service_close_issue.png) +![The GitLab integration user leaves a comment on JIRA](img/jira_issue_closed.png) --- +You can see from the above image that there are four references to GitLab: + +- The first is from a comment in a specific commit +- The second one is from the JIRA issue reference in the merge request + description +- The third is from the actual commit that solved the issue +- And the fourth one is from the commit that the merge request created + [services-templates]: ../project_services/services_templates.md "Services templates documentation" [JIRA Core]: https://www.atlassian.com/software/jira/core "The JIRA Core website" -- cgit v1.2.1 From 484d14680db012509a8d35092aebd5be714cf943 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 25 Jan 2016 13:34:13 -0800 Subject: Update rails, rails-html-sanitizer, and nokogiri for security fixes See https://dev.gitlab.org/gitlab/gitlabhq/issues/2643 --- Gemfile | 5 +++-- Gemfile.lock | 66 ++++++++++++++++++++++++++++++------------------------------ 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/Gemfile b/Gemfile index 1d367441364..37aeb09e93c 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source "https://rubygems.org" -gem 'rails', '4.2.5' +gem 'rails', '4.2.5.1' gem 'rails-deprecated_sanitizer', '~> 1.0.3' # Responders respond_to and respond_with @@ -103,7 +103,8 @@ gem 'asciidoctor', '~> 1.5.2' gem 'rouge', '~> 1.10.1' # See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s -gem 'nokogiri', '1.6.7.1' +# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM +gem 'nokogiri', '1.6.7.2' # Diffs gem 'diffy', '~> 3.0.3' diff --git a/Gemfile.lock b/Gemfile.lock index d809cfdb4f0..87895c55886 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,41 +4,41 @@ GEM CFPropertyList (2.3.2) RedCloth (4.2.9) ace-rails-ap (2.0.1) - actionmailer (4.2.5) - actionpack (= 4.2.5) - actionview (= 4.2.5) - activejob (= 4.2.5) + actionmailer (4.2.5.1) + actionpack (= 4.2.5.1) + actionview (= 4.2.5.1) + activejob (= 4.2.5.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.5) - actionview (= 4.2.5) - activesupport (= 4.2.5) + actionpack (4.2.5.1) + actionview (= 4.2.5.1) + activesupport (= 4.2.5.1) rack (~> 1.6) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.5) - activesupport (= 4.2.5) + actionview (4.2.5.1) + activesupport (= 4.2.5.1) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - activejob (4.2.5) - activesupport (= 4.2.5) + activejob (4.2.5.1) + activesupport (= 4.2.5.1) globalid (>= 0.3.0) - activemodel (4.2.5) - activesupport (= 4.2.5) + activemodel (4.2.5.1) + activesupport (= 4.2.5.1) builder (~> 3.1) - activerecord (4.2.5) - activemodel (= 4.2.5) - activesupport (= 4.2.5) + activerecord (4.2.5.1) + activemodel (= 4.2.5.1) + activesupport (= 4.2.5.1) arel (~> 6.0) activerecord-deprecated_finders (1.0.4) activerecord-session_store (0.1.2) actionpack (>= 4.0.0, < 5) activerecord (>= 4.0.0, < 5) railties (>= 4.0.0, < 5) - activesupport (4.2.5) + activesupport (4.2.5.1) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) @@ -482,7 +482,7 @@ GEM grape newrelic_rpm newrelic_rpm (3.9.4.245) - nokogiri (1.6.7.1) + nokogiri (1.6.7.2) mini_portile2 (~> 2.0.0.rc2) nprogress-rails (0.1.6.7) oauth (0.4.7) @@ -588,16 +588,16 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) - rails (4.2.5) - actionmailer (= 4.2.5) - actionpack (= 4.2.5) - actionview (= 4.2.5) - activejob (= 4.2.5) - activemodel (= 4.2.5) - activerecord (= 4.2.5) - activesupport (= 4.2.5) + rails (4.2.5.1) + actionmailer (= 4.2.5.1) + actionpack (= 4.2.5.1) + actionview (= 4.2.5.1) + activejob (= 4.2.5.1) + activemodel (= 4.2.5.1) + activerecord (= 4.2.5.1) + activesupport (= 4.2.5.1) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.5) + railties (= 4.2.5.1) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) @@ -605,11 +605,11 @@ GEM activesupport (>= 4.2.0.beta, < 5.0) nokogiri (~> 1.6.0) rails-deprecated_sanitizer (>= 1.0.1) - rails-html-sanitizer (1.0.2) + rails-html-sanitizer (1.0.3) loofah (~> 2.0) - railties (4.2.5) - actionpack (= 4.2.5) - activesupport (= 4.2.5) + railties (4.2.5.1) + actionpack (= 4.2.5.1) + activesupport (= 4.2.5.1) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.0.0) @@ -962,7 +962,7 @@ DEPENDENCIES net-ssh (~> 3.0.1) newrelic-grape newrelic_rpm (~> 3.9.4.245) - nokogiri (= 1.6.7.1) + nokogiri (= 1.6.7.2) nprogress-rails (~> 0.1.6.7) oauth2 (~> 1.0.0) octokit (~> 3.7.0) @@ -988,7 +988,7 @@ DEPENDENCIES rack-attack (~> 4.3.1) rack-cors (~> 0.4.0) rack-oauth2 (~> 1.2.1) - rails (= 4.2.5) + rails (= 4.2.5.1) rails-deprecated_sanitizer (~> 1.0.3) raphael-rails (~> 2.1.2) rblineprof -- cgit v1.2.1 From 6292ddd1980cad80e0f8385929cb07767b604efb Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 25 Jan 2016 22:36:44 +0100 Subject: Add references to the rouge gem library [ci skip] --- doc/markdown/markdown.md | 5 +++++ doc/profile/preferences.md | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index bc8e7d155e7..83c77742b19 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -88,6 +88,9 @@ GFM will autolink almost any URL you copy and paste into your text. ## Code and Syntax Highlighting +_GitLab uses the [rouge ruby library][rouge] for syntax highlighting. For a +list of supported languages visit the rouge website._ + Blocks of code are either fenced by lines with three back-ticks ```, or are indented with four spaces. Only the fenced code blocks support syntax highlighting. ```no-highlight @@ -585,3 +588,5 @@ By including colons in the header row, you can align the text within that column - This document leveraged heavily from the [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet). - The [Markdown Syntax Guide](https://daringfireball.net/projects/markdown/syntax) at Daring Fireball is an excellent resource for a detailed explanation of standard markdown. - [Dillinger.io](http://dillinger.io) is a handy tool for testing standard markdown. + +[rouge]: http://rouge.jneen.net/ "Rouge website" diff --git a/doc/profile/preferences.md b/doc/profile/preferences.md index f17bbe8f2aa..073b8797508 100644 --- a/doc/profile/preferences.md +++ b/doc/profile/preferences.md @@ -12,6 +12,9 @@ The default is **Charcoal**. ## Syntax highlighting theme +_GitLab uses the [rouge ruby library][rouge] for syntax highlighting. For a +list of supported languages visit the rouge website._ + Changing this setting allows the user to customize the theme used when viewing syntax highlighted code on the site. @@ -36,3 +39,5 @@ The default is **Your Projects**. It allows user to choose what content he or she want to see on project page. The default is **Readme**. + +[rouge]: http://rouge.jneen.net/ "Rouge website" -- cgit v1.2.1 From 81d79c7f0a8e8bd54cf39a886ab89ec4532f34b1 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 25 Jan 2016 23:52:04 +0100 Subject: Fix typos, grammar and styling --- doc/integration/external-issue-tracker.md | 2 +- doc/project_services/jira.md | 29 ++++++++++++----------------- doc/project_services/project_services.md | 2 +- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index e25af546527..a2d7e922aad 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -19,7 +19,7 @@ To enable an external issue tracker you must configure the appropriate **Service Visit the links below for details: - [Redmine](../project_services/redmine.md) -- [Jira](../proect_services/jira.md) +- [Jira](../project_services/jira.md) ### Service Template diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index b82e4857814..c733e4b2e9b 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -11,7 +11,7 @@ settings page in GitLab with a default template. To configure the JIRA template, see the [Services Templates documentation][services-templates]. Once the GitLab project is connected to JIRA, you can reference and close the -issues in JIRA directly from GitLab's Merge requests. +issues in JIRA directly from GitLab's merge requests. ## Configuration @@ -25,7 +25,7 @@ The configuration consists of two parts: First things first, we need to create a user in JIRA which will have access to all projects that need to integrate with GitLab. -We have split this stage in steps so it could be easier to follow. +We have split this stage in steps so it is easier to follow. --- @@ -36,7 +36,7 @@ We have split this stage in steps so it could be easier to follow. --- -1. The next step is to create a new user (e.g., `gitlab`) who has write-access +1. The next step is to create a new user (e.g., `gitlab`) who has write access to projects in JIRA. Enter the user's name and a _valid_ e-mail address since JIRA sends a verification e-mail to set-up the password. _**Note:** JIRA creates the username automatically by using the e-mail @@ -46,7 +46,7 @@ We have split this stage in steps so it could be easier to follow. --- -1. Now, let's create a `gitlab-developers` group which will have write-access +1. Now, let's create a `gitlab-developers` group which will have write access to projects in JIRA. Go to the **Groups** tab and select **Create group**. ![JIRA create new user](img/jira_create_new_group.png) @@ -67,7 +67,7 @@ We have split this stage in steps so it could be easier to follow. --- -1. Add the `gitlab` user to `gitlab-developers` group by going to +1. Add the `gitlab` user to the `gitlab-developers` group by going to **Users > GitLab user > Add group** and selecting the `gitlab-developers` group from the dropdown menu. Notice that the group says _Access_ which is what we aim for. @@ -99,9 +99,9 @@ Fill in the required details on the page, as described in the table below. | Setting | Description | | ------- | ----------- | | `Description` | A name for the issue tracker (to differentiate between instances, for example). | -| `Project url` | The URL to the JIRA project which is being linked to this GitLab project. It's of the form: `https:///issues/?jql=project=`. | +| `Project url` | The URL to the JIRA project which is being linked to this GitLab project. It is of the form: `https:///issues/?jql=project=`. | | `Issues url` | The URL to the JIRA project issues overview for the project that is linked to this GitLab project. It is of the form: `https:///browse/:id`. Leave `:id` as-is, it gets replaced by GitLab at runtime. | -| `New issue url` | This is the URL to create a new issue in JIRA for the project linked to this GitLab project, and is of the form: `https:///secure/CreateIssue.jspa` | +| `New issue url` | This is the URL to create a new issue in JIRA for the project linked to this GitLab project, and it is of the form: `https:///secure/CreateIssue.jspa` | | `Api url` | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https:///rest/api/2`. | | `Username` | The username of the user created in [configuring JIRA step](#configuring-jira). | | `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | @@ -126,12 +126,8 @@ In the unfortunate event that you are still using GitLab < 7.8, consult the By now you should have [configured JIRA](#configuring-jira) and enabled the [JIRA service in GitLab](#configuring-gitlab). If everything is set up correctly -you should be able to: - -- reference JIRA issues and -- close JIRA issues - -by just mentioning their ID in GitLab commits and merge requests. +you should be able to reference JIRA issues and close JIRA issues by just +mentioning their ID in GitLab commits and merge requests. ### Referencing JIRA Issues @@ -198,7 +194,7 @@ in GitLab. The workflow would be something like this: --- -In the following screenshot you can see how the link references to the JIRA +In the following screenshot you can see what the link references to the JIRA issue look like. ![JIRA - submit a GitLab merge request](img/jira_submit_gitlab_merge_request.png) @@ -215,10 +211,9 @@ with a link to the commit that resolved the issue. You can see from the above image that there are four references to GitLab: - The first is from a comment in a specific commit -- The second one is from the JIRA issue reference in the merge request - description +- The second is from the JIRA issue reference in the merge request description - The third is from the actual commit that solved the issue -- And the fourth one is from the commit that the merge request created +- And the fourth is from the commit that the merge request created [services-templates]: ../project_services/services_templates.md "Services templates documentation" [JIRA Core]: https://www.atlassian.com/software/jira/core "The JIRA Core website" diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index f004f3e8789..55db3e4f2f3 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -22,7 +22,7 @@ further configuration instructions and details. Contributions are welcome. | Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | | [HipChat](hipchat.md) | Private group chat and IM | | [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | -| [JIRA](jira.md) | Jira issue tracker | +| [JIRA](jira.md) | JIRA issue tracker | | JetBrains TeamCity CI | A continuous integration and build server | | PivotalTracker | Project Management Software (Source Commits Endpoint) | | Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | -- cgit v1.2.1 From 47ff1c56089b3df9c36b77c02f0f3db54fea1d54 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 25 Jan 2016 14:52:55 -0800 Subject: Add temporary monkey patch to get specs passing on 4.2.5.1 --- spec/spec_helper.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0225a0ee53f..8f381f46e57 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -48,4 +48,10 @@ FactoryGirl::SyntaxRunner.class_eval do include RSpec::Mocks::ExampleMethods end +# Work around a Rails 4.2.5.1 issue +# See https://github.com/rspec/rspec-rails/issues/1532 +RSpec::Rails::ViewRendering::EmptyTemplatePathSetDecorator.class_eval do + alias_method :find_all_anywhere, :find_all +end + ActiveRecord::Migration.maintain_test_schema! -- cgit v1.2.1 From fbc988de54e7e87cde2828a43942181907a42c7d Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 25 Jan 2016 23:59:28 +0100 Subject: Remove old JIRA reference --- doc/project_services/jira.md | 14 ++------------ doc/project_services/jira_old.md | 40 ---------------------------------------- 2 files changed, 2 insertions(+), 52 deletions(-) delete mode 100644 doc/project_services/jira_old.md diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index c733e4b2e9b..17b9d59d439 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -81,16 +81,14 @@ password as they will be needed when configuring GitLab in the next section. ### Configuring GitLab +_**Note:** The currently supported JIRA versions are v6.x and v7.x._ + Assuming you [have already configured JIRA](#configuring-jira), now it's time to configure GitLab. JIRA configuration in GitLab is done via a project's [**Services**](../project_services/project_services.md). -#### GitLab 7.8 and up - -_**Note:** The currently supported JIRA versions are v6.x and v7.x._ - To enable JIRA integration in a project, navigate to the project's **Settings > Services > JIRA**. @@ -114,14 +112,6 @@ with the linked JIRA project. --- -#### GitLab 6.x-7.7 with JIRA v6.x - -_**Note:** GitLab versions 7.8 and up contain various integration improvements. -We strongly recommend upgrading._ - -In the unfortunate event that you are still using GitLab < 7.8, consult the -[jira_old document](jira_old.md) on how to configure JIRA. - ## JIRA issues By now you should have [configured JIRA](#configuring-jira) and enabled the diff --git a/doc/project_services/jira_old.md b/doc/project_services/jira_old.md deleted file mode 100644 index 2813b142de2..00000000000 --- a/doc/project_services/jira_old.md +++ /dev/null @@ -1,40 +0,0 @@ -# GitLab 6.x-7.7 with JIRA v6.x - -**NOTE: This method is deprecated. GitLab versions 7.8 and up, contain various -integration improvements and we strongly recommend upgrading. The official -supported document on JIRA integration can be found under [JIRA](jira.md).** - ---- - -In `gitlab.yml` enable the JIRA issue tracker section by -[uncommenting these lines][jira-gitlab-yml]. This will make sure that all -issues within GitLab are pointing to the JIRA issue tracker. - -After you set this, you will be able to close issues in JIRA by a commit in -GitLab. - -Go to your project's **Settings** page and fill in the project name for the -JIRA project: - -![Set the JIRA project name in GitLab to 'NEW'](img/jira_project_name.png) - ---- - -You can also enable the JIRA service that will allow you to interact with JIRA -issues. Go to the **Settings > Services > JIRA** and: - -1. Tick the active check box to enable the service -1. Supply the URL to JIRA server, for example https://jira.example.com -1. Supply the username of a user we created under `Configuring JIRA` section, - for example `gitlab` -1. Supply the password of the user -1. Optional: supply the JIRA API version, default is version `2` -1. Optional: supply the JIRA issue transition ID (issue transition to closed). - This is dependent on JIRA settings, default is `2` -1. Hit save - - -![JIRA services page](img/jira_service.png) - - -[jira-gitlab-yml]: https://gitlab.com/subscribers/gitlab-ee/blob/6-8-stable-ee/config/gitlab.yml.example#L111-115 -- cgit v1.2.1 From 229845947f09e158937f114e0cedf486252bdda5 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 25 Jan 2016 23:59:49 +0100 Subject: Add minimum required GitLab version [ci skip] --- doc/project_services/jira.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index 17b9d59d439..d6b2e7f521b 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -81,7 +81,10 @@ password as they will be needed when configuring GitLab in the next section. ### Configuring GitLab -_**Note:** The currently supported JIRA versions are v6.x and v7.x._ +_**Note:** The currently supported JIRA versions are v6.x and v7.x. and GitLab +7.8 or higher is required._ + +--- Assuming you [have already configured JIRA](#configuring-jira), now it's time to configure GitLab. @@ -116,8 +119,8 @@ with the linked JIRA project. By now you should have [configured JIRA](#configuring-jira) and enabled the [JIRA service in GitLab](#configuring-gitlab). If everything is set up correctly -you should be able to reference JIRA issues and close JIRA issues by just -mentioning their ID in GitLab commits and merge requests. +you should be able to reference and close JIRA issues by just mentioning their +ID in GitLab commits and merge requests. ### Referencing JIRA Issues -- cgit v1.2.1 From 46c36e0e01b21f0c2f42bfb366b56d30de43c3f1 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 25 Jan 2016 21:00:23 -0200 Subject: Fixi import redirect loop --- app/controllers/projects/imports_controller.rb | 20 +++- .../projects/imports_controller_spec.rb | 109 +++++++++++++++++++++ 2 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 spec/controllers/projects/imports_controller_spec.rb diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index 8d8035ef5ff..07f355c35b1 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -1,8 +1,8 @@ class Projects::ImportsController < Projects::ApplicationController # Authorize before_action :authorize_admin_project! - before_action :require_no_repo, except: :show - before_action :redirect_if_progress, except: :show + before_action :require_no_repo, only: [:new, :create] + before_action :redirect_if_progress, only: [:new, :create] def new end @@ -24,11 +24,11 @@ class Projects::ImportsController < Projects::ApplicationController end def show - if @project.repository_exists? || @project.import_finished? + if @project.import_finished? if continue_params redirect_to continue_params[:to], notice: continue_params[:notice] else - redirect_to project_path(@project), notice: "The project was successfully forked." + redirect_to namespace_project_path(@project.namespace, @project), notice: finished_notice end elsif @project.import_failed? redirect_to new_namespace_project_import_path(@project.namespace, @project) @@ -36,6 +36,7 @@ class Projects::ImportsController < Projects::ApplicationController if continue_params && continue_params[:notice_now] flash.now[:notice] = continue_params[:notice_now] end + # Render end end @@ -44,6 +45,7 @@ class Projects::ImportsController < Projects::ApplicationController def continue_params continue_params = params[:continue] + if continue_params continue_params.permit(:to, :notice, :notice_now) else @@ -51,8 +53,16 @@ class Projects::ImportsController < Projects::ApplicationController end end + def finished_notice + if @project.forked? + 'The project was successfully forked.' + else + 'The project was successfully imported.' + end + end + def require_no_repo - if @project.repository_exists? && !@project.import_in_progress? + if @project.repository_exists? redirect_to(namespace_project_path(@project.namespace, @project)) end end diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb new file mode 100644 index 00000000000..85d1d1e0524 --- /dev/null +++ b/spec/controllers/projects/imports_controller_spec.rb @@ -0,0 +1,109 @@ +require 'spec_helper' + +describe Projects::ImportsController do + let(:user) { create(:user) } + + describe 'GET #show' do + context 'when repository does not exists' do + let(:project) { create(:empty_project) } + + before do + sign_in(user) + project.team << [user, :master] + end + + it 'renders template' do + get :show, namespace_id: project.namespace.to_param, project_id: project.to_param + + expect(response).to render_template :show + end + + it 'sets flash.now if params is present' do + get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { notice_now: 'Started' } + + expect(flash.now[:notice]).to eq 'Started' + end + end + + context 'when repository exists' do + let(:project) { create(:project_empty_repo, import_url: 'https://github.com/vim/vim.git') } + + before do + sign_in(user) + project.team << [user, :master] + end + + context 'when import is in progress' do + before do + project.update_attribute(:import_status, :started) + end + + it 'renders template' do + get :show, namespace_id: project.namespace.to_param, project_id: project.to_param + + expect(response).to render_template :show + end + + it 'sets flash.now if params is present' do + get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { notice_now: 'In progress' } + + expect(flash.now[:notice]).to eq 'In progress' + end + end + + context 'when import failed' do + before do + project.update_attribute(:import_status, :failed) + end + + it 'redirects to new_namespace_project_import_path' do + get :show, namespace_id: project.namespace.to_param, project_id: project.to_param + + expect(response).to redirect_to new_namespace_project_import_path(project.namespace, project) + end + end + + context 'when import finished' do + before do + project.update_attribute(:import_status, :finished) + end + + context 'when project is a fork' do + it 'redirects to namespace_project_path' do + allow_any_instance_of(Project).to receive(:forked?).and_return(true) + + get :show, namespace_id: project.namespace.to_param, project_id: project.to_param + + expect(flash[:notice]).to eq 'The project was successfully forked.' + expect(response).to redirect_to namespace_project_path(project.namespace, project) + end + end + + context 'when project is external' do + it 'redirects to namespace_project_path' do + get :show, namespace_id: project.namespace.to_param, project_id: project.to_param + + expect(flash[:notice]).to eq 'The project was successfully imported.' + expect(response).to redirect_to namespace_project_path(project.namespace, project) + end + end + + context 'when continue params is present' do + let(:params) do + { + to: namespace_project_path(project.namespace, project), + notice: 'Finished' + } + end + + it 'redirects to params[:to]' do + get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: params + + expect(flash[:notice]).to eq params[:notice] + expect(response).to redirect_to params[:to] + end + end + end + end + end +end -- cgit v1.2.1 From 6dd88e090e94f7f36fafd3e35c35a2868f89eebe Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 21 Jan 2016 16:09:32 -0200 Subject: Extract Projects::ImportService service from RepositoryImportWorker --- app/services/projects/import_service.rb | 71 +++++++++++++++++ app/workers/repository_import_worker.rb | 46 ++--------- spec/services/projects/import_service_spec.rb | 106 ++++++++++++++++++++++++++ 3 files changed, 184 insertions(+), 39 deletions(-) create mode 100644 app/services/projects/import_service.rb create mode 100644 spec/services/projects/import_service_spec.rb diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb new file mode 100644 index 00000000000..7a9508ef085 --- /dev/null +++ b/app/services/projects/import_service.rb @@ -0,0 +1,71 @@ +module Projects + class ImportService < BaseService + include Gitlab::ShellAdapter + + class Error < StandardError; end + + ALLOWED_TYPES = [ + 'bitbucket', + 'fogbugz', + 'gitlab', + 'github', + 'google_code' + ] + + def execute + if unknown_url? + # In this case, we only want to import issues, not a repository. + create_repository + else + import_repository + end + + import_data + + success + rescue Error => e + error(e.message) + end + + private + + def create_repository + unless project.create_repository + raise Error, 'The repository could not be created.' + end + end + + def import_repository + begin + gitlab_shell.import_repository(project.path_with_namespace, project.import_url) + rescue Gitlab::Shell::Error => e + raise Error, e.message + end + end + + def import_data + return unless has_importer? + + unless importer.execute + raise Error, 'The remote data could not be imported.' + end + + if project.import_type == 'bitbucket' + Gitlab::BitbucketImport::KeyDeleter.new(project).execute + end + end + + def has_importer? + ALLOWED_TYPES.include?(project.import_type) + end + + def importer + class_name = "Gitlab::#{project.import_type.camelize}Import::Importer" + class_name.constantize.new(project) + end + + def unknown_url? + project.import_url == Project::UNKNOWN_IMPORT_URL + end + end +end diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index d18c0706b30..e295a9ddd14 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -4,52 +4,20 @@ class RepositoryImportWorker sidekiq_options queue: :gitlab_shell - def perform(project_id) - project = Project.find(project_id) + attr_accessor :project, :current_user - if project.import_url == Project::UNKNOWN_IMPORT_URL - # In this case, we only want to import issues, not a repository. - unless project.create_repository - project.update(import_error: "The repository could not be created.") - project.import_fail - return - end - else - begin - gitlab_shell.import_repository(project.path_with_namespace, project.import_url) - rescue Gitlab::Shell::Error => e - project.update(import_error: e.message) - project.import_fail - return - end - end + def perform(project_id) + @project = Project.find(project_id) + @current_user = @project.creator - data_import_result = - case project.import_type - when 'github' - Gitlab::GithubImport::Importer.new(project).execute - when 'gitlab' - Gitlab::GitlabImport::Importer.new(project).execute - when 'bitbucket' - Gitlab::BitbucketImport::Importer.new(project).execute - when 'google_code' - Gitlab::GoogleCodeImport::Importer.new(project).execute - when 'fogbugz' - Gitlab::FogbugzImport::Importer.new(project).execute - else - true - end + result = Projects::ImportService.new(project, current_user).execute - unless data_import_result - project.update(import_error: "The remote issue data could not be imported.") + if result[:status] == :error + project.update(import_error: result[:message]) project.import_fail return end - if project.import_type == 'bitbucket' - Gitlab::BitbucketImport::KeyDeleter.new(project).execute - end - project.import_finish end end diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb new file mode 100644 index 00000000000..04f474c736c --- /dev/null +++ b/spec/services/projects/import_service_spec.rb @@ -0,0 +1,106 @@ +require 'spec_helper' + +describe Projects::ImportService, services: true do + let!(:project) { create(:empty_project) } + let(:user) { project.creator } + + subject { described_class.new(project, user) } + + describe '#execute' do + context 'with unknown url' do + before do + project.import_url = Project::UNKNOWN_IMPORT_URL + end + + it 'succeeds if repository is created successfully' do + expect(project).to receive(:create_repository).and_return(true) + + result = subject.execute + + expect(result[:status]).to eq :success + end + + it 'fails if repository creation fails' do + expect(project).to receive(:create_repository).and_return(false) + + result = subject.execute + + expect(result[:status]).to eq :error + expect(result[:message]).to eq 'The repository could not be created.' + end + end + + context 'with known url' do + before do + project.import_url = 'https://github.com/vim/vim.git' + end + + it 'succeeds if repository import is successfully' do + expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_return(true) + + result = subject.execute + + expect(result[:status]).to eq :success + end + + it 'fails if repository import fails' do + expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_raise(Gitlab::Shell::Error.new('Failed to import the repository')) + + result = subject.execute + + expect(result[:status]).to eq :error + expect(result[:message]).to eq 'Failed to import the repository' + end + end + + context 'with valid importer' do + before do + stub_github_omniauth_provider + + project.import_url = 'https://github.com/vim/vim.git' + project.import_type = 'github' + + allow(project).to receive(:import_data).and_return(double.as_null_object) + end + + it 'succeeds if importer succeeds' do + expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_return(true) + expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(true) + + result = subject.execute + + expect(result[:status]).to eq :success + end + + it 'fails if importer fails' do + expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_return(true) + expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(false) + + result = subject.execute + + expect(result[:status]).to eq :error + expect(result[:message]).to eq 'The remote data could not be imported.' + end + + it 'fails if importer raise an error' do + expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_return(true) + expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_raise(Projects::ImportService::Error.new('Github: failed to connect API')) + + result = subject.execute + + expect(result[:status]).to eq :error + expect(result[:message]).to eq 'Github: failed to connect API' + end + end + + def stub_github_omniauth_provider + provider = OpenStruct.new( + name: 'github', + app_id: 'asd123', + app_secret: 'asd123' + ) + + Gitlab.config.omniauth.providers << provider + end + end +end -- cgit v1.2.1 From 05697adb42acc943d666f629ddfa07f7cc9e636c Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 1 Jan 2016 18:23:47 -0500 Subject: Limit jquery-ui requires to what we actually use This commit is a smoke test to run CI and see if we get back any JS errors. --- app/assets/javascripts/application.js.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 48c9890cfb5..d5e6ff0717a 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -5,7 +5,10 @@ # the compiled file. # #= require jquery -#= require jquery-ui +#= require jquery-ui/autocomplete +#= require jquery-ui/datepicker +#= require jquery-ui/effect-highlight +#= require jquery-ui/sortable #= require jquery_ujs #= require jquery.cookie #= require jquery.endless-scroll -- cgit v1.2.1 From 28aa7158118d73e60a86e8986e499e13197c4069 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Mon, 25 Jan 2016 22:34:34 -0500 Subject: Clicks the edit button instead of opening the dropdown Fixes #2307 --- app/assets/javascripts/shortcuts_issuable.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/shortcuts_issuable.coffee b/app/assets/javascripts/shortcuts_issuable.coffee index bb532194682..f717a753cf8 100644 --- a/app/assets/javascripts/shortcuts_issuable.coffee +++ b/app/assets/javascripts/shortcuts_issuable.coffee @@ -5,11 +5,11 @@ class @ShortcutsIssuable extends ShortcutsNavigation constructor: (isMergeRequest) -> super() Mousetrap.bind('a', -> - $('.js-assignee').select2('open') + $('.block.assignee .edit-link').trigger('click') return false ) Mousetrap.bind('m', -> - $('.js-milestone').select2('open') + $('.block.milestone .edit-link').trigger('click') return false ) Mousetrap.bind('r', => -- cgit v1.2.1 From 9b7dd5a4ee2fee8f57a720c6942a0126b982cfcf Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Mon, 25 Jan 2016 23:18:51 -0500 Subject: Makes ol margin big enough for 3 digits. Fixes #7960 --- app/assets/stylesheets/framework/typography.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 4866a17005d..8d8f41287da 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -115,7 +115,7 @@ ul, ol { padding: 0; - margin: 6px 0 6px 18px !important; + margin: 6px 0 6px 28px !important; } li { -- cgit v1.2.1 From 5e88d7357f48bec823c27abb0776aa217672d979 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Mon, 25 Jan 2016 23:51:40 -0500 Subject: Placeholder now visible completely. Fixes #2498 --- app/assets/stylesheets/pages/groups.scss | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 263993f59a5..fdd86979a36 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -1,5 +1,15 @@ .member-search-form { float: left; + + input[type='search'] { + width: 225px; + vertical-align: bottom; + + @media (max-width: $screen-xs-max) { + width: 100px; + vertical-align: bottom; + } + } } .milestone-row { -- cgit v1.2.1 From fa9c6fa0caae511a37c0b134eafd9cb94a7cbc7a Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 25 Jan 2016 20:59:24 -0800 Subject: Update CHANGELOG [ci skip] --- CHANGELOG | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index b3b4aa380d5..8d1f8d59b3c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,12 @@ v 8.5.0 (unreleased) - Whitelist raw "abbr" elements when parsing Markdown (Benedict Etzel) - Don't vendor minified JS +v 8.4.1 + - Apply security updates for Rails (4.2.5.1), rails-html-sanitizer (1.0.3), + and Nokogiri (1.6.7.2) + - Fix redirect loop during import + - Fix diff highlighting for all syntax themes + v 8.4.0 - Allow LDAP users to change their email if it was not set by the LDAP server - Ensure Gravatar host looks like an actual host -- cgit v1.2.1 From 55e031a0d0b3a96b6e4b867d4e934572726bcd63 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 26 Jan 2016 00:25:41 -0500 Subject: Adds feature: Ajax Close/Open updates discussion automatically Triggers jQuery event Fixes #5544 --- app/assets/javascripts/issue.js.coffee | 1 + app/assets/javascripts/notes.js.coffee | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index cbc70cd846c..d663e34871c 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -50,6 +50,7 @@ class @Issue new Flash(issueFailMessage, 'alert') success: (data, textStatus, jqXHR) -> if data.saved + $(document).trigger('issuable:change'); if isClose $('a.btn-close').addClass('hidden') $('a.btn-reopen').removeClass('hidden') diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 53d72be66e3..e12eb857f80 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -64,6 +64,8 @@ class @Notes # fetch notes when tab becomes visible $(document).on "visibilitychange", @visibilityChange + $(document).on "issuable:change", @refresh + cleanBinding: -> $(document).off "ajax:success", ".js-main-target-form" $(document).off "ajax:success", ".js-discussion-note-form" -- cgit v1.2.1 From ed8256a28bcfffc9eb8b9f2d52b47dc01c543884 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 26 Jan 2016 00:28:27 -0500 Subject: Adds comment about event listener --- app/assets/javascripts/notes.js.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index e12eb857f80..3347ab65c90 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -64,6 +64,7 @@ class @Notes # fetch notes when tab becomes visible $(document).on "visibilitychange", @visibilityChange + # when issue status changes, we need to refresh data $(document).on "issuable:change", @refresh cleanBinding: -> -- cgit v1.2.1 From 8d397862cd90f39cc70dd775d613635a542ab72c Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sat, 23 Jan 2016 19:33:18 +0100 Subject: Use generic method to checks if artifacts are available --- app/views/projects/commit_statuses/_commit_status.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index 1736dccaf3c..2e3c956ddc4 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -66,7 +66,7 @@ %td .pull-right - - if current_user && can?(current_user, :read_build_artifacts, commit_status.project) && commit_status.artifacts? + - if current_user && can?(current_user, :read_build_artifacts, commit_status.project) && commit_status.artifacts_download_url = link_to commit_status.artifacts_download_url, title: 'Download artifacts' do %i.fa.fa-download - if current_user && can?(current_user, :manage_builds, commit_status.project) -- cgit v1.2.1 From 5583b9526baa17b3f2e86322ee36fc1f94b322dd Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 26 Jan 2016 08:32:36 +0100 Subject: Add specs for build created using generic commit status --- spec/factories/commit_statuses.rb | 9 ++- spec/features/commits_spec.rb | 127 ++++++++++++++++++++++---------------- 2 files changed, 81 insertions(+), 55 deletions(-) diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb index 8898b71e2a3..45b11edcef3 100644 --- a/spec/factories/commit_statuses.rb +++ b/spec/factories/commit_statuses.rb @@ -1,11 +1,16 @@ FactoryGirl.define do factory :commit_status, class: CommitStatus do - started_at 'Di 29. Okt 09:51:28 CET 2013' - finished_at 'Di 29. Okt 09:53:28 CET 2013' name 'default' + ref 'master' status 'success' description 'commit status' commit factory: :ci_commit_with_one_job + started_at 'Tue, 26 Jan 2016 08:23:42 +0100' + finished_at 'Tue, 26 Jan 2016 08:23:42 +0100' + + after(:build) do |build, evaluator| + build.project = build.commit.project + end factory :generic_commit_status, class: GenericCommitStatus do name 'generic' diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index fe7f07f5b75..5a62da10619 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -16,83 +16,104 @@ describe 'Commits' do FactoryGirl.create :ci_commit, project: project, sha: project.commit.sha end - let!(:build) { FactoryGirl.create :ci_build, commit: commit } + context 'commit status is Generic Commit Status' do + let!(:status) { FactoryGirl.create :generic_commit_status, commit: commit } - describe 'Project commits' do - before do - visit namespace_project_commits_path(project.namespace, project, :master) - end + describe 'Commit builds' do + before do + visit ci_status_path(commit) + end - it 'should show build status' do - page.within("//li[@id='commit-#{commit.short_sha}']") do - expect(page).to have_css(".ci-status-link") + it { expect(page).to have_content commit.sha[0..7] } + + it 'contains generic commit status build' do + page.within('.table-holder') do + expect(page).to have_content "##{status.id}" # build id + expect(page).to have_content 'generic' # build name + end end end end - describe 'Commit builds' do - before do - visit ci_status_path(commit) - end - - it { expect(page).to have_content commit.sha[0..7] } - it { expect(page).to have_content commit.git_commit_message } - it { expect(page).to have_content commit.git_author_name } - end + context 'commit status is Ci Build' do + let!(:build) { FactoryGirl.create :ci_build, commit: commit } - context 'Download artifacts' do - let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } + describe 'Project commits' do + before do + visit namespace_project_commits_path(project.namespace, project, :master) + end - before do - build.update_attributes(artifacts_file: artifacts_file) + it 'should show build status' do + page.within("//li[@id='commit-#{commit.short_sha}']") do + expect(page).to have_css(".ci-status-link") + end + end end - it do - visit ci_status_path(commit) - click_on 'Download artifacts' - expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) - end - end + describe 'Commit builds' do + before do + visit ci_status_path(commit) + end - describe 'Cancel all builds' do - it 'cancels commit' do - visit ci_status_path(commit) - click_on 'Cancel running' - expect(page).to have_content 'canceled' + it { expect(page).to have_content commit.sha[0..7] } + it { expect(page).to have_content commit.git_commit_message } + it { expect(page).to have_content commit.git_author_name } end - end - describe 'Cancel build' do - it 'cancels build' do - visit ci_status_path(commit) - click_on 'Cancel' - expect(page).to have_content 'canceled' - end - end + context 'Download artifacts' do + let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } + + before do + build.update_attributes(artifacts_file: artifacts_file) + end - describe '.gitlab-ci.yml not found warning' do - context 'ci builds enabled' do - it "does not show warning" do + it do visit ci_status_path(commit) - expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' + click_on 'Download artifacts' + expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) end + end - it 'shows warning' do - stub_ci_commit_yaml_file(nil) + describe 'Cancel all builds' do + it 'cancels commit' do visit ci_status_path(commit) - expect(page).to have_content '.gitlab-ci.yml not found in this commit' + click_on 'Cancel running' + expect(page).to have_content 'canceled' end end - context 'ci builds disabled' do - before do - stub_ci_builds_disabled - stub_ci_commit_yaml_file(nil) + describe 'Cancel build' do + it 'cancels build' do visit ci_status_path(commit) + click_on 'Cancel' + expect(page).to have_content 'canceled' end + end + + describe '.gitlab-ci.yml not found warning' do + context 'ci builds enabled' do + it "does not show warning" do + visit ci_status_path(commit) + expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' + end + + it 'shows warning' do + stub_ci_commit_yaml_file(nil) + visit ci_status_path(commit) + expect(page).to have_content '.gitlab-ci.yml not found in this commit' + end + end + + context 'ci builds disabled' do + before do + stub_ci_builds_disabled + stub_ci_commit_yaml_file(nil) + visit ci_status_path(commit) + end - it 'does not show warning' do - expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' + it 'does not show warning' do + expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' + end end end end -- cgit v1.2.1 From 88d59d0161ac2f4890cfe77bb81c687a561b17ac Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 26 Jan 2016 08:36:54 +0100 Subject: Add Changelog entry for undefined method fix in commit builds --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 8d1f8d59b3c..ed018e5ebbb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,9 @@ v 8.5.0 (unreleased) - Whitelist raw "abbr" elements when parsing Markdown (Benedict Etzel) - Don't vendor minified JS +v 8.4.2 (unreleased) + - Fix method undefined when using external commit status in builds + v 8.4.1 - Apply security updates for Rails (4.2.5.1), rails-html-sanitizer (1.0.3), and Nokogiri (1.6.7.2) -- cgit v1.2.1 From 89154880e56c34e94cdd7326c6cfc67bdb256f74 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Tue, 26 Jan 2016 09:45:12 +0100 Subject: Save button on app settings now btn-save --- app/views/admin/application_settings/_form.html.haml | 2 +- app/views/admin/applications/_form.html.haml | 2 +- app/views/admin/groups/_form.html.haml | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index c4020c8273b..dde35f8742a 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -268,4 +268,4 @@ = f.text_field :sentry_dsn, class: 'form-control' .form-actions - = f.submit 'Save', class: 'btn btn-primary' + = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml index fa4e6335c73..e18f7b499dd 100644 --- a/app/views/admin/applications/_form.html.haml +++ b/app/views/admin/applications/_form.html.haml @@ -22,5 +22,5 @@ %code= Doorkeeper.configuration.native_redirect_uri for local tests .form-actions - = f.submit 'Submit', class: "btn btn-primary wide" + = f.submit 'Submit', class: "btn btn-save wide" = link_to "Cancel", admin_applications_path, class: "btn btn-default" diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 8de2ba74a79..198026a1f75 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -21,6 +21,5 @@ - else .form-actions - = f.submit 'Save changes', class: "btn btn-primary" + = f.submit 'Save changes', class: "btn btn-save" = link_to 'Cancel', admin_group_path(@group), class: "btn btn-cancel" - -- cgit v1.2.1 From 128a6411d2a60c855e3c99303d0157f436e32f13 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 25 Jan 2016 16:56:23 +0100 Subject: Don't pluck project IDs for events By instead using a sub-query we save ourselves the overhead of loading any data into memory only to pass it on to another query. --- app/controllers/dashboard/projects_controller.rb | 2 +- app/controllers/dashboard_controller.rb | 6 +++--- app/controllers/groups_controller.rb | 6 +----- app/models/event.rb | 6 +++++- app/models/project.rb | 2 ++ 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index 58e9049f158..0b7fcdf5e9e 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -36,7 +36,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController private def load_events - @events = Event.in_projects(@projects.pluck(:id)) + @events = Event.in_projects(@projects) @events = @event_filter.apply_filter(@events).with_associations @events = @events.limit(20).offset(params[:offset] || 0) end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 087da935087..139e40db180 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -23,14 +23,14 @@ class DashboardController < Dashboard::ApplicationController protected def load_events - project_ids = + projects = if params[:filter] == "starred" current_user.starred_projects else current_user.authorized_projects - end.pluck(:id) + end - @events = Event.in_projects(project_ids) + @events = Event.in_projects(projects) @events = @event_filter.apply_filter(@events).with_associations @events = @events.limit(20).offset(params[:offset] || 0) end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index fb26a4e6fc3..7bbef5ab29c 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -87,10 +87,6 @@ class GroupsController < Groups::ApplicationController @projects ||= ProjectsFinder.new.execute(current_user, group: group).sorted_by_activity.non_archived end - def project_ids - @projects.pluck(:id) - end - # Dont allow unauthorized access to group def authorize_read_group! unless @group and (@projects.present? or can?(current_user, :read_group, @group)) @@ -123,7 +119,7 @@ class GroupsController < Groups::ApplicationController end def load_events - @events = Event.in_projects(project_ids) + @events = Event.in_projects(@projects) @events = event_filter.apply_filter(@events).with_associations @events = @events.limit(20).offset(params[:offset] || 0) end diff --git a/app/models/event.rb b/app/models/event.rb index 01d008035a5..8dfc1d04709 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -47,7 +47,11 @@ class Event < ActiveRecord::Base # Scopes scope :recent, -> { reorder(id: :desc) } scope :code_push, -> { where(action: PUSHED) } - scope :in_projects, ->(project_ids) { where(project_id: project_ids).recent } + + scope :in_projects, ->(projects) do + where(project_id: projects.reorder(nil).id_only).recent + end + scope :with_associations, -> { includes(project: :namespace) } scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) } diff --git a/app/models/project.rb b/app/models/project.rb index 4bd51449c25..63ecf5b7105 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -215,6 +215,8 @@ class Project < ActiveRecord::Base scope :public_and_internal_only, -> { where(visibility_level: Project.public_and_internal_levels) } scope :non_archived, -> { where(archived: false) } + scope :id_only, -> { select(:id) } + state_machine :import_status, initial: :none do event :import_start do transition [:none, :finished] => :started -- cgit v1.2.1 From 89aeed183ccbadc9580945a68b8df83f50856c2c Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 26 Jan 2016 12:16:47 +0100 Subject: feature and fix for 500 on group missing --- app/controllers/groups_controller.rb | 1 + features/explore/groups.feature | 5 +++++ features/steps/explore/groups.rb | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index fb26a4e6fc3..828e5509262 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -81,6 +81,7 @@ class GroupsController < Groups::ApplicationController def group @group ||= Group.find_by(path: params[:id]) + render_404 unless @group end def load_projects diff --git a/features/explore/groups.feature b/features/explore/groups.feature index 5fc9b135601..7529dcc3d9d 100644 --- a/features/explore/groups.feature +++ b/features/explore/groups.feature @@ -3,6 +3,11 @@ Feature: Explore Groups Background: Given group "TestGroup" has private project "Enterprise" + Scenario: I should not see a group if it does not exist + When I sign in as a user + And I visit group "NonExistentGroup" page + Then page status code should be 404 + Scenario: I should see group with private and internal projects as user Given group "TestGroup" has internal project "Internal" When I sign in as a user diff --git a/features/steps/explore/groups.rb b/features/steps/explore/groups.rb index 87f32e70d59..68c473e7b29 100644 --- a/features/steps/explore/groups.rb +++ b/features/steps/explore/groups.rb @@ -26,6 +26,10 @@ class Spinach::Features::ExploreGroups < Spinach::FeatureSteps visit group_path(Group.find_by(name: "TestGroup")) end + step 'I visit group "NonExistentGroup" page' do + visit group_path(-1) + end + step 'I visit group "TestGroup" issues page' do visit issues_group_path(Group.find_by(name: "TestGroup")) end -- cgit v1.2.1 From c0403234193dcb2033bd57160bb0ab6893bb8d77 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 21 Jan 2016 16:53:34 -0200 Subject: Move Gitlab::BitbucketImport::KeyDeleter to it's own importer --- app/services/projects/import_service.rb | 4 --- lib/gitlab/bitbucket_import/importer.rb | 47 ++++++++++++++++++++------------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index 7a9508ef085..2015897dd19 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -49,10 +49,6 @@ module Projects unless importer.execute raise Error, 'The remote data could not be imported.' end - - if project.import_type == 'bitbucket' - Gitlab::BitbucketImport::KeyDeleter.new(project).execute - end end def has_importer? diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 2355b3c6ddc..e8c6d89764c 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -13,12 +13,34 @@ module Gitlab end def execute - project_identifier = project.import_source + import_issues if has_issues? - return true unless client.project(project_identifier)["has_issues"] + true + ensure + Gitlab::BitbucketImport::KeyDeleter.new(project).execute + end - #Issues && Comments - issues = client.issues(project_identifier) + private + + def gl_user_id(project, bitbucket_id) + if bitbucket_id + user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s) + (user && user.id) || project.creator_id + else + project.creator_id + end + end + + def identifier + project.import_source + end + + def has_issues? + client.project(identifier)["has_issues"] + end + + def import_issues + issues = client.issues(identifier) issues.each do |issue| body = '' @@ -33,7 +55,7 @@ module Gitlab body = @formatter.author_line(author) body += issue["content"] - comments = client.issue_comments(project_identifier, issue["local_id"]) + comments = client.issue_comments(identifier, issue["local_id"]) if comments.any? body += @formatter.comments_header @@ -56,20 +78,9 @@ module Gitlab author_id: gl_user_id(project, reporter) ) end - - true + rescue ActiveRecord::RecordInvalid => e + raise Projects::ImportService::Error, e.message end - - private - - def gl_user_id(project, bitbucket_id) - if bitbucket_id - user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s) - (user && user.id) || project.creator_id - else - project.creator_id - end - end end end end -- cgit v1.2.1 From b58a2e30b214ffc98f492aab88137cc3fd48355d Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Fri, 22 Jan 2016 13:00:00 -0200 Subject: Wrap errors on GitHub importer to raise Projects::ImportService::Error --- lib/gitlab/bitbucket_import/importer.rb | 2 ++ lib/gitlab/github_import/importer.rb | 17 ++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index e8c6d89764c..3f483847efa 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -16,6 +16,8 @@ module Gitlab import_issues if has_issues? true + rescue ActiveRecord::RecordInvalid => e + raise Projects::ImportService::Error.new, e.message ensure Gitlab::BitbucketImport::KeyDeleter.new(project).execute end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 663402e8197..e2a85f29825 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -35,8 +35,8 @@ module Gitlab end true - rescue ActiveRecord::RecordInvalid - false + rescue ActiveRecord::RecordInvalid => e + raise Projects::ImportService::Error, e.message end def import_pull_requests @@ -53,8 +53,8 @@ module Gitlab end true - rescue ActiveRecord::RecordInvalid - false + rescue ActiveRecord::RecordInvalid => e + raise Projects::ImportService::Error, e.message end def import_comments(issue_number, noteable) @@ -83,10 +83,13 @@ module Gitlab true rescue Gitlab::Shell::Error => e - if e.message =~ /repository not exported/ - true + # GitHub error message when the wiki repo has not been created, + # this means that repo has wiki enabled, but have no pages. So, + # we can skip the import. + if e.message !~ /repository not exported/ + raise Projects::ImportService::Error, e.message else - false + true end end end -- cgit v1.2.1 From 4941f64653eb336ec1b9e05fb184199a9b2a7836 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Fri, 22 Jan 2016 13:00:40 -0200 Subject: Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index b3b4aa380d5..5505f2dbd9c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,7 @@ v 8.5.0 (unreleased) - Fix diff comments loaded by AJAX to load comment with diff in discussion tab - Whitelist raw "abbr" elements when parsing Markdown (Benedict Etzel) - Don't vendor minified JS + - Track project import failure v 8.4.0 - Allow LDAP users to change their email if it was not set by the LDAP server -- cgit v1.2.1 From a14089485ff697d58dddb61e6da2d1b6c64a803e Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 26 Jan 2016 08:45:18 -0500 Subject: Styles for file list change. New download btn WIP. --- app/assets/stylesheets/framework/buttons.scss | 4 ++ app/assets/stylesheets/pages/tree.scss | 6 +++ .../repositories/_download_archive.html.haml | 60 +++++++++------------- app/views/projects/tree/show.html.haml | 3 +- 4 files changed, 36 insertions(+), 37 deletions(-) diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index c99292c3f83..64854a67678 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -191,3 +191,7 @@ @include btn-green } } + +.btn-icon { + +} \ No newline at end of file diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 6a6dd7dfc85..4ff549ade1f 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -14,9 +14,15 @@ .tree-table { margin-bottom: 0; + tr:nth-child(odd) { + background-color: #F7F9FB; + } + tr { > td, > th { line-height: 26px; + border-top: 1px solid #EEF0F2; + border-bottom: 1px solid #EEF0F2; } &:hover { diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml index b9486a9b492..fb4250be901 100644 --- a/app/views/projects/repositories/_download_archive.html.haml +++ b/app/views/projects/repositories/_download_archive.html.haml @@ -1,37 +1,27 @@ - ref = ref || nil - btn_class = btn_class || '' -- split_button = split_button || false -- if split_button == true - %span.btn-group{class: btn_class} - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn col-xs-10', rel: 'nofollow' do - %i.fa.fa-download - %span Download zip - %a.col-xs-2.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } - %span.caret - %span.sr-only - Select Archive Format - %ul.col-xs-10.dropdown-menu{ role: 'menu' } - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), rel: 'nofollow' do - %i.fa.fa-download - %span Download zip - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar.gz - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar.bz2 - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar -- else - %span.btn-group{class: btn_class} - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do - %i.fa.fa-download - %span zip - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), class: 'btn', rel: 'nofollow' do - %i.fa.fa-download - %span tar.gz + +.btn-group + %button.btn.dropdown-toggle{ 'data-toggle' => 'dropdown', class: btn_class } + =icon('download') + Download + %span.caret + %span.sr-only + Select Archive Format + %ul.col-xs-10.dropdown-menu{ role: 'menu' } + %li + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), rel: 'nofollow' do + %i.fa.fa-download + %span Download zip + %li + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar.gz + %li + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar.bz2 + %li + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar \ No newline at end of file diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index 91fb2a44594..3e9256f6d5f 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -6,9 +6,8 @@ = render 'projects/last_push' .tree-controls - = render 'projects/find_file_link' - if can? current_user, :download_code, @project - = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true + = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped btn-success' #tree-holder.tree-holder.clearfix .nav-block -- cgit v1.2.1 From c10296f92d581f54cbd32c156806ad72eddcab77 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Tue, 26 Jan 2016 09:36:13 +0100 Subject: Fix visibility level texts on application settings Introduced by me through !2005 --- CHANGELOG | 1 + app/views/admin/application_settings/_form.html.haml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2a4b32f2519..858c5dd96a2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,7 @@ v 8.5.0 (unreleased) - Whitelist raw "abbr" elements when parsing Markdown (Benedict Etzel) - Don't vendor minified JS - Track project import failure + - Fix visibility level text in admin area (Zeger-Jan van de Weg) v 8.4.1 - Apply security updates for Rails (4.2.5.1), rails-html-sanitizer (1.0.3), diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index dde35f8742a..baadca09518 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -14,11 +14,11 @@ .form-group.project-visibility-level-holder = f.label :default_project_visibility, class: 'control-label col-sm-2' .col-sm-10 - = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project) + = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new) .form-group.project-visibility-level-holder = f.label :default_snippet_visibility, class: 'control-label col-sm-2' .col-sm-10 - = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: PersonalSnippet) + = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: ProjectSnippet.new) .form-group = f.label :restricted_visibility_levels, class: 'control-label col-sm-2' .col-sm-10 -- cgit v1.2.1 From e6f3fe5d3b16809634dd655b3d265c683b538b12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 26 Jan 2016 15:11:15 +0100 Subject: Ensure rake tasks that don't need a DB connection can be run without one When using ActiveRecord::Base.connection.active? without a DB connection, we get a "PG::ConnectionBad: could not connect to server" error because ActiveRecord::Base.connection doesn't exist. By using ActiveRecord::Base.connected? we ensure we don't get this error if the connection is missing, which is the all point of the Gitlab::CurrentSettings#connect_to_db? method! --- CHANGELOG | 1 + lib/gitlab/current_settings.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index b3b4aa380d5..97606659f35 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.5.0 (unreleased) + - Ensure rake tasks that don't need a DB connection can be run without one - Add "visibility" flag to GET /projects api endpoint - Ignore binary files in code search to prevent Error 500 (Stan Hu) - Upgrade gitlab_git to 7.2.23 to fix commit message mentions in first branch push diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 429f1f9bb56..19b7427256c 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -38,7 +38,7 @@ module Gitlab true end - use_db && ActiveRecord::Base.connection.active? && + use_db && ActiveRecord::Base.connected? && # The following condition is important: if a migrations adds a # column to the application_settings table and a validation in # the ApplicationSetting uses this new column we might end-up in -- cgit v1.2.1 From 966176512797f037eb933691a2a21a8c3bb280b7 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 26 Jan 2016 15:23:57 +0100 Subject: Update commit status factory to reflect recent changes --- spec/factories/commit_statuses.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb index 45b11edcef3..b7c2b32cb13 100644 --- a/spec/factories/commit_statuses.rb +++ b/spec/factories/commit_statuses.rb @@ -1,11 +1,10 @@ FactoryGirl.define do factory :commit_status, class: CommitStatus do name 'default' - ref 'master' status 'success' description 'commit status' commit factory: :ci_commit_with_one_job - started_at 'Tue, 26 Jan 2016 08:23:42 +0100' + started_at 'Tue, 26 Jan 2016 08:21:42 +0100' finished_at 'Tue, 26 Jan 2016 08:23:42 +0100' after(:build) do |build, evaluator| -- cgit v1.2.1 From d6a186135f320904507bf9f195fc9637719552d0 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 26 Jan 2016 15:43:09 +0100 Subject: fix for failing spec --- app/controllers/groups_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 828e5509262..f7c6607aff7 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -81,7 +81,8 @@ class GroupsController < Groups::ApplicationController def group @group ||= Group.find_by(path: params[:id]) - render_404 unless @group + return @group if @group + render_404 end def load_projects -- cgit v1.2.1 From 758177d14cd946ed1bd8d9bd9609d9d8d4a378dc Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Mon, 25 Jan 2016 20:08:29 +0100 Subject: Warn admin of granting admin rights during OAuth Fixes #3951 Warning style the same as the warning a user sees on a archived project. --- CHANGELOG | 1 + app/views/doorkeeper/authorizations/new.html.haml | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 858c5dd96a2..b7eddb25bce 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,7 @@ v 8.4.1 and Nokogiri (1.6.7.2) - Fix redirect loop during import - Fix diff highlighting for all syntax themes + - Warn admin during OAuth of granting admin rights (Zeger-Jan van de Weg) v 8.4.0 - Allow LDAP users to change their email if it was not set by the LDAP server diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml index 15f9ee266c1..185182a10de 100644 --- a/app/views/doorkeeper/authorizations/new.html.haml +++ b/app/views/doorkeeper/authorizations/new.html.haml @@ -4,6 +4,13 @@ Authorize %strong.text-info= @pre_auth.client.name to use your account? + + - if current_user.admin? + .text-warning.prepend-top-20 + %p + = icon("exclamation-triangle fw") + Caution! You are admin, and thus will grant admin permission to #{@pre_auth.client.name}. + - if @pre_auth.scopes #oauth-permissions %p This application will be able to: @@ -25,4 +32,4 @@ = hidden_field_tag :state, @pre_auth.state = hidden_field_tag :response_type, @pre_auth.response_type = hidden_field_tag :scope, @pre_auth.scope - = submit_tag "Deny", class: "btn btn-danger prepend-left-10" \ No newline at end of file + = submit_tag "Deny", class: "btn btn-danger prepend-left-10" -- cgit v1.2.1 From 103d67768d5dc3b3f2676d3f4bc7e2fb5581083e Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Tue, 26 Jan 2016 16:36:59 +0100 Subject: Improve text on warning message --- app/views/doorkeeper/authorizations/new.html.haml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml index 185182a10de..eae80e5210f 100644 --- a/app/views/doorkeeper/authorizations/new.html.haml +++ b/app/views/doorkeeper/authorizations/new.html.haml @@ -9,7 +9,9 @@ .text-warning.prepend-top-20 %p = icon("exclamation-triangle fw") - Caution! You are admin, and thus will grant admin permission to #{@pre_auth.client.name}. + You are an admin, which means granting access to + %strong #{@pre_auth.client.name} + will allow them to interact with GitLab as an admin as well. Proceed with caution. - if @pre_auth.scopes #oauth-permissions -- cgit v1.2.1 From 111834883d287c89a24ae395900786d771214bdb Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 26 Jan 2016 11:20:18 -0500 Subject: Adjusts styles of table tree Fixes #12725 --- app/assets/stylesheets/framework/buttons.scss | 7 +++-- app/assets/stylesheets/pages/tree.scss | 37 ++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 64854a67678..0e67e35b606 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -127,6 +127,9 @@ &.btn-xs { margin-right: 3px; } + i { + font-size: 11px; + } } &.disabled { pointer-events: auto !important; @@ -190,8 +193,4 @@ .btn-green { @include btn-green } -} - -.btn-icon { - } \ No newline at end of file diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 4ff549ade1f..ab12f2d55e0 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -1,6 +1,12 @@ .tree-holder { > .nav-block { - margin: 11px 0; + margin: 22px 0 0 0; + background: #F8FAFC; + padding: 9px 9px 4px 14px; + border: 1px solid #EBECF0; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + border-bottom-width: 0; } .file-finder { @@ -13,7 +19,18 @@ .tree-table { margin-bottom: 0; - + border: 1px solid #EEF0F2; + thead { + th { + background-color: white; + + &:last-child { + .light { + color: #8C9AAC; + } + } + } + } tr:nth-child(odd) { background-color: #F7F9FB; } @@ -28,8 +45,6 @@ &:hover { td { background: $hover; - border-top: 1px solid #ADF; - border-bottom: 1px solid #ADF; } cursor: pointer; } @@ -48,10 +63,14 @@ max-width: 320px; vertical-align: middle; - i, a { + a { color: $gl-link-color; } + i { + color: #797B7D; + } + img { position: relative; top:-1px; @@ -60,10 +79,16 @@ .tree_commit { max-width: 320px; + a { + color: #8C9AAC; + } } .tree_time_ago { min-width: 135px; + time { + color: #8C9AAC; + } } } @@ -129,5 +154,5 @@ .tree-controls { float: right; - margin-top: 11px; + margin: 32px 7px 0 0; } -- cgit v1.2.1 From 9a1ec97fc47c7d621d3b3332a21c9c9be589ab79 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 26 Jan 2016 11:52:09 -0500 Subject: Revert "Adjusts styles of table tree" This reverts commit 111834883d287c89a24ae395900786d771214bdb. --- app/assets/stylesheets/framework/buttons.scss | 7 ++--- app/assets/stylesheets/pages/tree.scss | 37 +++++---------------------- 2 files changed, 10 insertions(+), 34 deletions(-) diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 0e67e35b606..64854a67678 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -127,9 +127,6 @@ &.btn-xs { margin-right: 3px; } - i { - font-size: 11px; - } } &.disabled { pointer-events: auto !important; @@ -193,4 +190,8 @@ .btn-green { @include btn-green } +} + +.btn-icon { + } \ No newline at end of file diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index ab12f2d55e0..4ff549ade1f 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -1,12 +1,6 @@ .tree-holder { > .nav-block { - margin: 22px 0 0 0; - background: #F8FAFC; - padding: 9px 9px 4px 14px; - border: 1px solid #EBECF0; - border-top-left-radius: 3px; - border-top-right-radius: 3px; - border-bottom-width: 0; + margin: 11px 0; } .file-finder { @@ -19,18 +13,7 @@ .tree-table { margin-bottom: 0; - border: 1px solid #EEF0F2; - thead { - th { - background-color: white; - - &:last-child { - .light { - color: #8C9AAC; - } - } - } - } + tr:nth-child(odd) { background-color: #F7F9FB; } @@ -45,6 +28,8 @@ &:hover { td { background: $hover; + border-top: 1px solid #ADF; + border-bottom: 1px solid #ADF; } cursor: pointer; } @@ -63,14 +48,10 @@ max-width: 320px; vertical-align: middle; - a { + i, a { color: $gl-link-color; } - i { - color: #797B7D; - } - img { position: relative; top:-1px; @@ -79,16 +60,10 @@ .tree_commit { max-width: 320px; - a { - color: #8C9AAC; - } } .tree_time_ago { min-width: 135px; - time { - color: #8C9AAC; - } } } @@ -154,5 +129,5 @@ .tree-controls { float: right; - margin: 32px 7px 0 0; + margin-top: 11px; } -- cgit v1.2.1 From 2b98b7faa10d235734d53c15e1c6f47c88db871f Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 26 Jan 2016 11:52:30 -0500 Subject: Revert "Styles for file list change." This reverts commit a14089485ff697d58dddb61e6da2d1b6c64a803e. --- app/assets/stylesheets/framework/buttons.scss | 4 -- app/assets/stylesheets/pages/tree.scss | 6 --- .../repositories/_download_archive.html.haml | 60 +++++++++++++--------- app/views/projects/tree/show.html.haml | 3 +- 4 files changed, 37 insertions(+), 36 deletions(-) diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 64854a67678..c99292c3f83 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -191,7 +191,3 @@ @include btn-green } } - -.btn-icon { - -} \ No newline at end of file diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 4ff549ade1f..6a6dd7dfc85 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -14,15 +14,9 @@ .tree-table { margin-bottom: 0; - tr:nth-child(odd) { - background-color: #F7F9FB; - } - tr { > td, > th { line-height: 26px; - border-top: 1px solid #EEF0F2; - border-bottom: 1px solid #EEF0F2; } &:hover { diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml index fb4250be901..b9486a9b492 100644 --- a/app/views/projects/repositories/_download_archive.html.haml +++ b/app/views/projects/repositories/_download_archive.html.haml @@ -1,27 +1,37 @@ - ref = ref || nil - btn_class = btn_class || '' - -.btn-group - %button.btn.dropdown-toggle{ 'data-toggle' => 'dropdown', class: btn_class } - =icon('download') - Download - %span.caret - %span.sr-only - Select Archive Format - %ul.col-xs-10.dropdown-menu{ role: 'menu' } - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), rel: 'nofollow' do - %i.fa.fa-download - %span Download zip - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar.gz - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar.bz2 - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar \ No newline at end of file +- split_button = split_button || false +- if split_button == true + %span.btn-group{class: btn_class} + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn col-xs-10', rel: 'nofollow' do + %i.fa.fa-download + %span Download zip + %a.col-xs-2.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } + %span.caret + %span.sr-only + Select Archive Format + %ul.col-xs-10.dropdown-menu{ role: 'menu' } + %li + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), rel: 'nofollow' do + %i.fa.fa-download + %span Download zip + %li + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar.gz + %li + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar.bz2 + %li + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar +- else + %span.btn-group{class: btn_class} + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do + %i.fa.fa-download + %span zip + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), class: 'btn', rel: 'nofollow' do + %i.fa.fa-download + %span tar.gz diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index 3e9256f6d5f..91fb2a44594 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -6,8 +6,9 @@ = render 'projects/last_push' .tree-controls + = render 'projects/find_file_link' - if can? current_user, :download_code, @project - = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped btn-success' + = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true #tree-holder.tree-holder.clearfix .nav-block -- cgit v1.2.1 From dd60590a3c90d5d12c4aba8a78338da5dd85310f Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 26 Jan 2016 11:54:31 -0500 Subject: Fix border color. Uses `$border-color` --- app/assets/stylesheets/pages/tree.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 6a6dd7dfc85..11e6d67a082 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -17,6 +17,7 @@ tr { > td, > th { line-height: 26px; + border-bottom: 1px solid $border-color; } &:hover { -- cgit v1.2.1 From 224c3197e159ea37e730f8b277a2bebe6f9ace03 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 26 Jan 2016 17:57:06 +0100 Subject: Bump Workhorse version to 0.6.2 Fix for #12634 has been introduced in gitlab-org/gitlab-workhorse!34. --- GITLAB_WORKHORSE_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index ee6cdce3c29..b6160487433 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -0.6.1 +0.6.2 -- cgit v1.2.1 From 33af1478439b06810ed09260d7393e891ac5c07e Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 26 Jan 2016 18:01:34 +0100 Subject: Add Changelog entry for missing artifacts in browser fix This has been fixed in Workhorse [ci skip] --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 858c5dd96a2..e20330cd60a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,9 @@ v 8.5.0 (unreleased) - Track project import failure - Fix visibility level text in admin area (Zeger-Jan van de Weg) +v 8.4.2 (unreleased) + - Fix missing artifacts in build artifacts browser (fixed in Workhorse) + v 8.4.1 - Apply security updates for Rails (4.2.5.1), rails-html-sanitizer (1.0.3), and Nokogiri (1.6.7.2) -- cgit v1.2.1 From 5ea32ba4982aef37f2a43b0324d45033992a9a60 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 26 Jan 2016 19:05:04 +0100 Subject: Fix hot reloading for CI API --- lib/ci/api/api.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb index 5c347e432b4..4e85d2c3c74 100644 --- a/lib/ci/api/api.rb +++ b/lib/ci/api/api.rb @@ -25,7 +25,7 @@ module Ci format :json - helpers Helpers + helpers ::Ci::API::Helpers helpers ::API::Helpers helpers Gitlab::CurrentSettings -- cgit v1.2.1 From 2cc20853c97b48b9353fae2bdb0c1f51d73bb33c Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 26 Jan 2016 14:40:52 -0500 Subject: adds border color to tables globally. --- app/assets/stylesheets/framework/tables.scss | 2 +- app/assets/stylesheets/pages/tree.scss | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index b5134f44ded..75b770ae5a2 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -38,7 +38,7 @@ table { td { border-color: $table-border-color; - border-bottom: 1px solid; + border-bottom: 1px solid $border-color; } } } diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 11e6d67a082..c7411617cb3 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -17,14 +17,11 @@ tr { > td, > th { line-height: 26px; - border-bottom: 1px solid $border-color; } &:hover { td { background: $hover; - border-top: 1px solid #ADF; - border-bottom: 1px solid #ADF; } cursor: pointer; } -- cgit v1.2.1 From a98bf00ca6be419ba3406dcce0d78d23b297d006 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 26 Jan 2016 14:51:55 -0500 Subject: Adds margin top to readme holder so not flush. --- app/assets/stylesheets/framework/files.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index a4791cf6b34..00cb756b376 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -7,6 +7,7 @@ border: 1px solid $border-color; &.readme-holder { + margin-top: 10px; border-bottom: 0; } -- cgit v1.2.1 From 4be65c3231f1bc260134d1c301b24baf87ef1552 Mon Sep 17 00:00:00 2001 From: Blake Hitchcock Date: Mon, 25 Jan 2016 11:27:03 -0500 Subject: Update ExternalIssue regex for JIRA integration The pattern in the `::reference_pattern` class method in the ExternalIssue model does not match all valid forms of JIRA project names. I have updated the regex to match JIRA project names with numbers and underscores. More information on valid JIRA project names can be found here: https://confluence.atlassian.com/jira/changing-the-project-key-format-192534.html * The first character must be a letter, * All letters used in the project key must be from the Modern Roman Alphabet and upper case, and * Only letters, numbers or the underscore character can be used. --- CHANGELOG | 1 + app/models/external_issue.rb | 2 +- config/initializers/1_settings.rb | 2 +- spec/lib/gitlab/closing_issue_extractor_spec.rb | 11 +++++++++++ spec/models/external_issue_spec.rb | 15 +++++++++++++++ 5 files changed, 29 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e20330cd60a..6660b33bb69 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -19,6 +19,7 @@ v 8.4.1 and Nokogiri (1.6.7.2) - Fix redirect loop during import - Fix diff highlighting for all syntax themes + - Update the ExternalIssue regex pattern (Blake Hitchcock) v 8.4.0 - Allow LDAP users to change their email if it was not set by the LDAP server diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index 49f6c95e045..2ca79df0a29 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -31,7 +31,7 @@ class ExternalIssue # Pattern used to extract `JIRA-123` issue references from text def self.reference_pattern - %r{(?([A-Z\-]+-)\d+)} + %r{(?\b([A-Z][A-Z0-9_]+-)\d+)} end def to_reference(_from_project = nil) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 04a7c16ebde..d8170557f7e 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -176,7 +176,7 @@ Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled']. Settings.gitlab['twitter_sharing_enabled'] ||= true if Settings.gitlab['twitter_sharing_enabled'].nil? Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], []) Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil? -Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z]*-\d*))+)' if Settings.gitlab['issue_closing_pattern'].nil? +Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' if Settings.gitlab['issue_closing_pattern'].nil? Settings.gitlab['default_projects_features'] ||= {} Settings.gitlab['webhook_timeout'] ||= 10 Settings.gitlab['max_attachment_size'] ||= 10 diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 99288da1e43..04cf11fc6f1 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -135,6 +135,17 @@ describe Gitlab::ClosingIssueExtractor, lib: true do message = "resolve #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + + context 'with an external issue tracker reference' do + it 'extracts the referenced issue' do + jira_project = create(:jira_project, name: 'JIRA_EXT1') + jira_issue = ExternalIssue.new("#{jira_project.name}-1", project: jira_project) + closing_issue_extractor = described_class.new jira_project + message = "Resolve #{jira_issue.to_reference}" + + expect(closing_issue_extractor.closed_by_message(message)).to eq([jira_issue]) + end + end end context "with a cross-project reference" do diff --git a/spec/models/external_issue_spec.rb b/spec/models/external_issue_spec.rb index 6ec6b9037a4..9b144dd1ecc 100644 --- a/spec/models/external_issue_spec.rb +++ b/spec/models/external_issue_spec.rb @@ -10,6 +10,21 @@ describe ExternalIssue, models: true do it { is_expected.to include_module(Referable) } end + describe '.reference_pattern' do + it 'allows underscores in the project name' do + expect(ExternalIssue.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' + end + + it 'allows numbers in the project name' do + expect(ExternalIssue.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234' + end + + it 'requires the project name to begin with A-Z' do + expect(ExternalIssue.reference_pattern.match('3EXT_EXT-1234')).to eq nil + expect(ExternalIssue.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' + end + end + describe '#to_reference' do it 'returns a String reference to the object' do expect(issue.to_reference).to eq issue.id -- cgit v1.2.1 From 84427dc6d5cd708834316888f611ce5c5347f9bc Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 26 Jan 2016 16:18:10 -0800 Subject: Update CHANGELOG [ci skip] --- CHANGELOG | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6660b33bb69..8e70081e7cc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,16 +10,22 @@ v 8.5.0 (unreleased) - Don't vendor minified JS - Track project import failure - Fix visibility level text in admin area (Zeger-Jan van de Weg) + - Update the ExternalIssue regex pattern (Blake Hitchcock) v 8.4.2 (unreleased) - - Fix missing artifacts in build artifacts browser (fixed in Workhorse) + - Bump required gitlab-workhorse version to bring in a fix for missing + artifacts in the build artifacts browser + - Get rid of those ugly borders on the file tree view + - Bump gitlab_git version to 7.2.24 in order to bring in a performance + improvement when checking if a repository was empty + - Add instrumentation for Gitlab::Git::Repository instance methods so we can + track them in Performance Monitoring. v 8.4.1 - Apply security updates for Rails (4.2.5.1), rails-html-sanitizer (1.0.3), and Nokogiri (1.6.7.2) - Fix redirect loop during import - Fix diff highlighting for all syntax themes - - Update the ExternalIssue regex pattern (Blake Hitchcock) v 8.4.0 - Allow LDAP users to change their email if it was not set by the LDAP server -- cgit v1.2.1 From a427ab9faa9dbc33e1b2a0c86c92360ef546524c Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 27 Jan 2016 09:01:05 +0100 Subject: Fix typo on artifacts doc [ci skip] --- doc/ci/build_artifacts/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/ci/build_artifacts/README.md b/doc/ci/build_artifacts/README.md index 58cbe653c34..71db5aa5dc8 100644 --- a/doc/ci/build_artifacts/README.md +++ b/doc/ci/build_artifacts/README.md @@ -13,8 +13,8 @@ ability of downloading the files separately. **Note:** The artifacts browser will be available only for new artifacts that are sent -to GitLab using GitLab Runner version 1.0 and up. You will not be available to -see the browser for old artifacts already uploaded to GitLab. +to GitLab using GitLab Runner version 1.0 and up. It will not be possible to +browse old artifacts already uploaded to GitLab. ## Enabling build artifacts -- cgit v1.2.1 From de7c9c7ab17a8b36ca0c645ba1a7a6971a077fb1 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 26 Jan 2016 11:54:57 +0100 Subject: Use Atom update times of the first event By simply loading the first event from the already sorted set we save ourselves extra (slow) queries just to get the latest update timestamp. This removes the need for Event.latest_update_time and significantly reduces the time needed to build an Atom feed. Fixes gitlab-org/gitlab-ce#12415 --- app/models/event.rb | 8 +------- app/models/project.rb | 2 -- app/views/dashboard/projects/index.atom.builder | 2 +- app/views/groups/show.atom.builder | 2 +- app/views/projects/show.atom.builder | 2 +- app/views/users/show.atom.builder | 2 +- spec/models/event_spec.rb | 21 --------------------- 7 files changed, 5 insertions(+), 34 deletions(-) diff --git a/app/models/event.rb b/app/models/event.rb index 8dfc1d04709..4be23a1cf72 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -49,7 +49,7 @@ class Event < ActiveRecord::Base scope :code_push, -> { where(action: PUSHED) } scope :in_projects, ->(projects) do - where(project_id: projects.reorder(nil).id_only).recent + where(project_id: projects.select(:id).reorder(nil)).recent end scope :with_associations, -> { includes(project: :namespace) } @@ -68,12 +68,6 @@ class Event < ActiveRecord::Base [Event::CREATED, Event::CLOSED, Event::MERGED]) end - def latest_update_time - row = select(:updated_at, :project_id).reorder(id: :desc).take - - row ? row.updated_at : nil - end - def limit_recent(limit = 20, offset = nil) recent.limit(limit).offset(offset) end diff --git a/app/models/project.rb b/app/models/project.rb index 63ecf5b7105..4bd51449c25 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -215,8 +215,6 @@ class Project < ActiveRecord::Base scope :public_and_internal_only, -> { where(visibility_level: Project.public_and_internal_levels) } scope :non_archived, -> { where(archived: false) } - scope :id_only, -> { select(:id) } - state_machine :import_status, initial: :none do event :import_start do transition [:none, :finished] => :started diff --git a/app/views/dashboard/projects/index.atom.builder b/app/views/dashboard/projects/index.atom.builder index 2e2712c5146..d4daf07c6c0 100644 --- a/app/views/dashboard/projects/index.atom.builder +++ b/app/views/dashboard/projects/index.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html" xml.id dashboard_projects_url - xml.updated @events.latest_update_time.xmlschema if @events.any? + xml.updated @events[0].updated_at.xmlschema if @events[0] @events.each do |event| event_to_atom(xml, event) diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder index 5cc0f5e1d2e..c66b82bb484 100644 --- a/app/views/groups/show.atom.builder +++ b/app/views/groups/show.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: group_url(@group, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: group_url(@group), rel: "alternate", type: "text/html" xml.id group_url(@group) - xml.updated @events.latest_update_time.xmlschema if @events.any? + xml.updated @events[0].updated_at.xmlschema if @events[0] @events.each do |event| event_to_atom(xml, event) diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder index d6762219108..2468509242a 100644 --- a/app/views/projects/show.atom.builder +++ b/app/views/projects/show.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html" xml.id namespace_project_url(@project.namespace, @project) - xml.updated @events.latest_update_time.xmlschema if @events.any? + xml.updated @events[0].updated_at.xmlschema if @events[0? @events.each do |event| event_to_atom(xml, event) diff --git a/app/views/users/show.atom.builder b/app/views/users/show.atom.builder index 114d1e7a379..e9e466c6350 100644 --- a/app/views/users/show.atom.builder +++ b/app/views/users/show.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml" xml.link href: user_url(@user), rel: "alternate", type: "text/html" xml.id user_url(@user) - xml.updated @events.latest_update_time.xmlschema if @events.any? + xml.updated @events[0].updated_at.xmlschema if @events[0] @events.each do |event| event_to_atom(xml, event) diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 071582b0282..ec2a923f91b 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -65,27 +65,6 @@ describe Event, models: true do it { expect(@event.author).to eq(@user) } end - describe '.latest_update_time' do - describe 'when events are present' do - let(:time) { Time.utc(2015, 1, 1) } - - before do - create(:closed_issue_event, updated_at: time) - create(:closed_issue_event, updated_at: time + 5) - end - - it 'returns the latest update time' do - expect(Event.latest_update_time).to eq(time + 5) - end - end - - describe 'when no events exist' do - it 'returns nil' do - expect(Event.latest_update_time).to be_nil - end - end - end - describe '.limit_recent' do let!(:event1) { create(:closed_issue_event) } let!(:event2) { create(:closed_issue_event) } -- cgit v1.2.1 From eef9c9d4476c46cf13215814944eed47576f862a Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 27 Jan 2016 10:44:24 +0100 Subject: update fix based on feedback --- app/controllers/groups_controller.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index f7c6607aff7..3d175a0717e 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -81,8 +81,7 @@ class GroupsController < Groups::ApplicationController def group @group ||= Group.find_by(path: params[:id]) - return @group if @group - render_404 + @group || render_404 end def load_projects -- cgit v1.2.1 From 5eb936dc318297473a40ab9cac06de106dc71c41 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 27 Jan 2016 11:23:28 +0100 Subject: moved and refactored feature based on feedback. Also updated CHANGELOG --- CHANGELOG | 1 + features/explore/groups.feature | 5 ----- features/groups.feature | 4 ++++ features/steps/explore/groups.rb | 4 ---- features/steps/groups.rb | 4 ++++ 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8d1f8d59b3c..a9102ea4206 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,7 @@ v 8.5.0 (unreleased) - Fix diff comments loaded by AJAX to load comment with diff in discussion tab - Whitelist raw "abbr" elements when parsing Markdown (Benedict Etzel) - Don't vendor minified JS + - Display 404 error on group not found v 8.4.1 - Apply security updates for Rails (4.2.5.1), rails-html-sanitizer (1.0.3), diff --git a/features/explore/groups.feature b/features/explore/groups.feature index 7529dcc3d9d..5fc9b135601 100644 --- a/features/explore/groups.feature +++ b/features/explore/groups.feature @@ -3,11 +3,6 @@ Feature: Explore Groups Background: Given group "TestGroup" has private project "Enterprise" - Scenario: I should not see a group if it does not exist - When I sign in as a user - And I visit group "NonExistentGroup" page - Then page status code should be 404 - Scenario: I should see group with private and internal projects as user Given group "TestGroup" has internal project "Internal" When I sign in as a user diff --git a/features/groups.feature b/features/groups.feature index c803e952980..55fffb012ae 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -3,6 +3,10 @@ Feature: Groups Given I sign in as "John Doe" And "John Doe" is owner of group "Owned" + Scenario: I should not see a group if it does not exist + When I visit group "NonExistentGroup" page + Then page status code should be 404 + Scenario: I should have back to group button When I visit group "Owned" page Then I should see back to dashboard button diff --git a/features/steps/explore/groups.rb b/features/steps/explore/groups.rb index 68c473e7b29..87f32e70d59 100644 --- a/features/steps/explore/groups.rb +++ b/features/steps/explore/groups.rb @@ -26,10 +26,6 @@ class Spinach::Features::ExploreGroups < Spinach::FeatureSteps visit group_path(Group.find_by(name: "TestGroup")) end - step 'I visit group "NonExistentGroup" page' do - visit group_path(-1) - end - step 'I visit group "TestGroup" issues page' do visit issues_group_path(Group.find_by(name: "TestGroup")) end diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 4c5122d1b7d..1e2a78a6029 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -120,6 +120,10 @@ class Spinach::Features::Groups < Spinach::FeatureSteps expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived') end + step 'I visit group "NonExistentGroup" page' do + visit group_path(-1) + end + private def assigned_to_me(key) -- cgit v1.2.1 From af52158ff83ac6de539af36eea729a59a23340fb Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 27 Jan 2016 11:26:04 +0100 Subject: Conform to doc styleguide on max line length --- doc/workflow/forking_workflow.md | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/doc/workflow/forking_workflow.md b/doc/workflow/forking_workflow.md index 8edf7c6ab3d..cf9dc01df53 100644 --- a/doc/workflow/forking_workflow.md +++ b/doc/workflow/forking_workflow.md @@ -1,36 +1,44 @@ # Project forking workflow -Forking a project to your own namespace is useful if you have no write access to the project you want to contribute -to. If you do have write access or can request it we recommend working together in the same repository since it is simpler. -See our **[GitLab Flow](https://about.gitlab.com/2014/09/29/gitlab-flow/)** article for more information about using -branches to work together. +Forking a project to your own namespace is useful if you have no write +access to the project you want to contribute to. If you do have write +access or can request it, we recommend working together in the same +repository since it is simpler. See our [GitLab Flow](gitlab_flow.md) +document more information about using branches to work together. ## Creating a fork -In order to create a fork of a project, all you need to do is click on the fork button located on the top right side -of the screen, close to the project's URL and right next to the stars button. +In order to create a fork of a project, all you need to do is click on +the fork button located on the top right side of the screen, close to +the project's URL and right next to the stars button. ![Fork button](forking/fork_button.png) -Once you do that you'll be presented with a screen where you can choose the namespace to fork to. Only namespaces -(groups and your own namespace) where you have write access to, will be shown. Click on the namespace to create your -fork there. +Once you do that you'll be presented with a screen where you can choose +the namespace to fork to. Only namespaces (groups and your own +namespace) where you have write access to, will be shown. Click on the +namespace to create your fork there. ![Groups view](forking/groups.png) -After the forking is done, you can start working on the newly created repository. There you will have full -[Owner](../permissions/permissions.md) access, so you can set it up as you please. +After the forking is done, you can start working on the newly created +repository. There you will have full +[Owner](../permissions/permissions.md) access, so you can set it up as +you please. ## Merging upstream -Once you are ready to send your code back to the main project, you need to create a merge request. Choose your forked -project's main branch as the source and the original project's main branch as the destination and create the merge request. +Once you are ready to send your code back to the main project, you need +to create a merge request. Choose your forked project's main branch as +the source and the original project's main branch as the destination and +create the merge request. ![Selecting branches](forking/branch_select.png) -You can then assign the merge request to someone to have them review your changes. Upon pressing the 'Accept Merge Request' -button, your changes will be added to the repository and branch you're merging into. +You can then assign the merge request to someone to have them review +your changes. Upon pressing the 'Accept Merge Request' button, your +changes will be added to the repository and branch you're merging into. ![New merge request](forking/merge_request.png) - +[gitlab flow]: https://about.gitlab.com/2014/09/29/gitlab-flow/ "GitLab Flow blog post" -- cgit v1.2.1 From b6662c2a8942b3412540219babe3d03511923ceb Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 27 Jan 2016 12:00:51 +0100 Subject: Refactor the forking process and add new images [ci skip] --- doc/workflow/forking/fork_button.png | Bin 68271 -> 0 bytes doc/workflow/forking/groups.png | Bin 98109 -> 0 bytes doc/workflow/forking_workflow.md | 41 ++++++++++++++------- .../img/forking_workflow_choose_namespace.png | Bin 0 -> 70405 bytes doc/workflow/img/forking_workflow_fork_button.png | Bin 0 -> 26438 bytes .../img/forking_workflow_path_taken_error.png | Bin 0 -> 22380 bytes 6 files changed, 28 insertions(+), 13 deletions(-) delete mode 100644 doc/workflow/forking/fork_button.png delete mode 100644 doc/workflow/forking/groups.png create mode 100644 doc/workflow/img/forking_workflow_choose_namespace.png create mode 100644 doc/workflow/img/forking_workflow_fork_button.png create mode 100644 doc/workflow/img/forking_workflow_path_taken_error.png diff --git a/doc/workflow/forking/fork_button.png b/doc/workflow/forking/fork_button.png deleted file mode 100644 index def4266476a..00000000000 Binary files a/doc/workflow/forking/fork_button.png and /dev/null differ diff --git a/doc/workflow/forking/groups.png b/doc/workflow/forking/groups.png deleted file mode 100644 index 3ac64b3c8e7..00000000000 Binary files a/doc/workflow/forking/groups.png and /dev/null differ diff --git a/doc/workflow/forking_workflow.md b/doc/workflow/forking_workflow.md index cf9dc01df53..217a4a4012f 100644 --- a/doc/workflow/forking_workflow.md +++ b/doc/workflow/forking_workflow.md @@ -8,30 +8,45 @@ document more information about using branches to work together. ## Creating a fork -In order to create a fork of a project, all you need to do is click on -the fork button located on the top right side of the screen, close to -the project's URL and right next to the stars button. +Forking a project is in most cases a two-step process. -![Fork button](forking/fork_button.png) -Once you do that you'll be presented with a screen where you can choose -the namespace to fork to. Only namespaces (groups and your own -namespace) where you have write access to, will be shown. Click on the -namespace to create your fork there. +1. Click on the fork button located in the middle of the page or a project's + home page right next to the stars button. -![Groups view](forking/groups.png) + ![Fork button](img/forking_workflow_fork_button.png) + + --- + +1. Once you do that, you'll be presented with a screen where you can choose + the namespace to fork to. Only namespaces (groups and your own + namespace) where you have write access to, will be shown. Click on the + namespace to create your fork there. + + ![Choose namespace](img/forking_workflow_choose_namespace.png) + + --- + + **Note:** + If the namespace you chose to fork the project to has another project with + the same path name, you will be presented with a warning that the forking + could not be completed. Try to resolve the error and repeat the forking + process. + + ![Path taken error](img/forking_workflow_path_taken_error.png) + + --- After the forking is done, you can start working on the newly created -repository. There you will have full -[Owner](../permissions/permissions.md) access, so you can set it up as -you please. +repository. There, you will have full [Owner](../permissions/permissions.md) +access, so you can set it up as you please. ## Merging upstream Once you are ready to send your code back to the main project, you need to create a merge request. Choose your forked project's main branch as the source and the original project's main branch as the destination and -create the merge request. +create the [merge request](merge_requests.md). ![Selecting branches](forking/branch_select.png) diff --git a/doc/workflow/img/forking_workflow_choose_namespace.png b/doc/workflow/img/forking_workflow_choose_namespace.png new file mode 100644 index 00000000000..eefe5769554 Binary files /dev/null and b/doc/workflow/img/forking_workflow_choose_namespace.png differ diff --git a/doc/workflow/img/forking_workflow_fork_button.png b/doc/workflow/img/forking_workflow_fork_button.png new file mode 100644 index 00000000000..49e68d33e89 Binary files /dev/null and b/doc/workflow/img/forking_workflow_fork_button.png differ diff --git a/doc/workflow/img/forking_workflow_path_taken_error.png b/doc/workflow/img/forking_workflow_path_taken_error.png new file mode 100644 index 00000000000..7a3139506fe Binary files /dev/null and b/doc/workflow/img/forking_workflow_path_taken_error.png differ -- cgit v1.2.1 From 95d2f0fb51de2c5800d41b7c4f285ed533ffe21a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 26 Jan 2016 19:25:48 +0100 Subject: Fix CI runner version not being properly updated when asking for a build Due to broken implementation of attribute_for_keys the runner information was not updated correctly. This MR adds test to check that such scenario will never happen again. --- CHANGELOG | 1 + lib/api/helpers.rb | 5 +++-- lib/ci/api/builds.rb | 2 +- lib/ci/api/helpers.rb | 10 +++++++--- lib/ci/api/runners.rb | 1 + spec/requests/ci/api/builds_spec.rb | 15 +++++++++++++++ spec/requests/ci/api/runners_spec.rb | 14 ++++++++++++++ 7 files changed, 42 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4b283f235a4..c2b43d32c48 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,7 @@ v 8.4.2 (unreleased) - Bump required gitlab-workhorse version to bring in a fix for missing artifacts in the build artifacts browser - Get rid of those ugly borders on the file tree view + - Fix updating the runner information when asking for builds - Bump gitlab_git version to 7.2.24 in order to bring in a performance improvement when checking if a repository was empty - Add instrumentation for Gitlab::Git::Repository instance methods so we can diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 3f528b9f7c0..9dacf7c1e86 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -153,10 +153,11 @@ module API end def attributes_for_keys(keys, custom_params = nil) + params_hash = custom_params || params attrs = {} keys.each do |key| - if params[key].present? or (params.has_key?(key) and params[key] == false) - attrs[key] = params[key] + if params_hash[key].present? or (params_hash.has_key?(key) and params_hash[key] == false) + attrs[key] = params_hash[key] end end ActionController::Parameters.new(attrs).permit! diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index 690bbf97a89..416b0b5f0b4 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -13,13 +13,13 @@ module Ci post "register" do authenticate_runner! update_runner_last_contact + update_runner_info required_attributes! [:token] not_found! unless current_runner.active? build = Ci::RegisterBuildService.new.execute(current_runner) if build - update_runner_info present build, with: Entities::BuildDetails else not_found! diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb index 1c91204e98c..199d62d9b8a 100644 --- a/lib/ci/api/helpers.rb +++ b/lib/ci/api/helpers.rb @@ -34,10 +34,14 @@ module Ci @runner ||= Runner.find_by_token(params[:token].to_s) end - def update_runner_info + def get_runner_version_from_params return unless params["info"].present? - info = attributes_for_keys(["name", "version", "revision", "platform", "architecture"], params["info"]) - current_runner.update(info) + attributes_for_keys(["name", "version", "revision", "platform", "architecture"], params["info"]) + end + + def update_runner_info + current_runner.assign_attributes(get_runner_version_from_params) + current_runner.save if current_runner.changed? end def max_artifacts_size diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb index bfc14fe7a6b..192b1d18a51 100644 --- a/lib/ci/api/runners.rb +++ b/lib/ci/api/runners.rb @@ -47,6 +47,7 @@ module Ci return forbidden! unless runner if runner.id + runner.update(get_runner_version_from_params) present runner, with: Entities::Runner else not_found! diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index eec927102aa..244947762dd 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -113,6 +113,21 @@ describe Ci::API::API do expect(json_response["depends_on_builds"].count).to eq(2) expect(json_response["depends_on_builds"][0]["name"]).to eq("rspec") end + + %w(name version revision platform architecture).each do |param| + context "updates runner #{param}" do + let(:value) { "#{param}_value" } + + subject { runner.read_attribute(param.to_sym) } + + it do + post ci_api("/builds/register"), token: runner.token, info: { param => value } + expect(response.status).to eq(404) + runner.reload + is_expected.to eq(value) + end + end + end end describe "PUT /builds/:id" do diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb index 5942aa7a1b5..db8189ffb79 100644 --- a/spec/requests/ci/api/runners_spec.rb +++ b/spec/requests/ci/api/runners_spec.rb @@ -51,6 +51,20 @@ describe Ci::API::API do expect(response.status).to eq(400) end + + %w(name version revision platform architecture).each do |param| + context "creates runner with #{param} saved" do + let(:value) { "#{param}_value" } + + subject { Ci::Runner.first.read_attribute(param.to_sym) } + + it do + post ci_api("/runners/register"), token: registration_token, info: { param => value } + expect(response.status).to eq(201) + is_expected.to eq(value) + end + end + end end describe "DELETE /runners/delete" do -- cgit v1.2.1 From a8fc4a01e26ab6be088ce6c1179770c757d805e1 Mon Sep 17 00:00:00 2001 From: Markus Fisch Date: Wed, 27 Jan 2016 12:16:37 +0000 Subject: Fixed typo for PRIVATE-TOKEN header Was PRIVATE_TOKEN but should be PRIVATE-TOKEN. Underscore vs. kebab-case. --- doc/api/builds.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/api/builds.md b/doc/api/builds.md index ecb50754c88..6e64d096644 100644 --- a/doc/api/builds.md +++ b/doc/api/builds.md @@ -18,7 +18,7 @@ GET /projects/:id/builds ### Example of request ``` -curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds" +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds" ``` ### Example of response @@ -123,7 +123,7 @@ GET /projects/:id/repository/commits/:sha/builds ### Example of request ``` -curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds" +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds" ``` ### Example of response @@ -213,7 +213,7 @@ GET /projects/:id/builds/:build_id ### Example of request ``` -curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8" +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8" ``` ### Example of response @@ -277,7 +277,7 @@ POST /projects/:id/builds/:build_id/cancel ### Example of request ``` -curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/cancel" +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/cancel" ``` ### Example of response @@ -327,7 +327,7 @@ POST /projects/:id/builds/:build_id/retry ### Example of request ``` -curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/retry" +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/retry" ``` ### Example of response -- cgit v1.2.1 From 1b84de573d0afe8c8d81db4ef2897a925686d212 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 27 Jan 2016 14:48:15 +0100 Subject: updated docs to reflect min requirement of 2.1 [ci skip] --- doc/install/installation.md | 2 +- doc/install/requirements.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index 1c0da6fbef4..f05097fedc5 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -107,7 +107,7 @@ Then select 'Internet Site' and press enter to confirm the hostname. ## 2. Ruby -_**Note:** The current supported Ruby version is 2.2.x. Ruby 2.3 is +_**Note:** The current supported Ruby versions are 2.1.x and 2.2.x. Ruby 2.3 is currently not supported._ The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab diff --git a/doc/install/requirements.md b/doc/install/requirements.md index fde1aaa90f3..006dae8ca9a 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -32,7 +32,7 @@ Please consider using a virtual machine to run GitLab. ## Ruby versions -GitLab requires Ruby (MRI) 2.2.x and currently does not work with version 2.3. +GitLab requires Ruby (MRI) 2.1.x or 2.2.x and currently does not work with version 2.3. You will have to use the standard MRI implementation of Ruby. We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab -- cgit v1.2.1 From 6e6b5df60c9b0a347d4b1635888dba8413e9eb21 Mon Sep 17 00:00:00 2001 From: Maverik Gately Date: Wed, 27 Jan 2016 14:27:57 +0000 Subject: add missing `RAILS_ENV=production` bit to bundle command --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1eabbdc5cad..1bd91f0bdce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -147,7 +147,7 @@ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true) sudo gitlab-rake gitlab:env:info) (For installations from source run and paste the output of: -sudo -u git -H bundle exec rake gitlab:env:info) +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production) ## Possible fixes -- cgit v1.2.1 From b8ed5789c03961444fc40baca31f870b30a73766 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 27 Jan 2016 15:41:05 +0100 Subject: update gitlab ci settings to include ruby 2.1 images --- .gitlab-ci.yml | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dca861faf12..414344a7f75 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -134,3 +134,119 @@ bundler:audit: - ruby - mysql allow_failure: true + +# Ruby 2.1 jobs + +spec:feature:ruby21: + image: ruby:2.1 + script: + - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature + tags: + - ruby + - mysql + only: + - master + +spec:api:ruby21: + image: ruby:2.1 + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api + tags: + - ruby + - mysql + only: + - master + +spec:models:ruby21: + image: ruby:2.1 + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models + tags: + - ruby + - mysql + only: + - master + +spec:lib:ruby21: + image: ruby:2.1 + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib + tags: + - ruby + - mysql + only: + - master + +spec:services:ruby21: + image: ruby:2.1 + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services + tags: + - ruby + - mysql + only: + - master + +spec:benchmark:ruby21: + image: ruby:2.1 + script: + - RAILS_ENV=test bundle exec rake spec:benchmark + tags: + - ruby + - mysql + allow_failure: true + only: + - master + +spec:other:ruby21: + image: ruby:2.1 + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other + tags: + - ruby + - mysql + only: + - master + +spinach:project:half:ruby21: + image: ruby:2.1 + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half + tags: + - ruby + - mysql + only: + - master + +spinach:project:rest:ruby21: + image: ruby:2.1 + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest + tags: + - ruby + - mysql + only: + - master + +spinach:other:ruby21: + image: ruby:2.1 + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other + tags: + - ruby + - mysql + only: + - master + +bundler:audit:ruby21: + image: ruby:2.1 + script: + - "bundle exec bundle-audit update" + - "bundle exec bundle-audit check" + tags: + - ruby + - mysql + allow_failure: true + only: + - master -- cgit v1.2.1 From 606f8e91905cfefd1f68044aeeedcb27e04fe081 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 27 Jan 2016 16:08:42 +0100 Subject: Increase contrast between highlighted code comments and inline diff marker. --- CHANGELOG | 1 + app/assets/stylesheets/highlight/dark.scss | 4 ++-- app/assets/stylesheets/highlight/monokai.scss | 4 ++-- app/assets/stylesheets/highlight/solarized_dark.scss | 4 ++-- app/assets/stylesheets/highlight/solarized_light.scss | 4 ++-- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4b283f235a4..b52a6b33e9c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,6 +20,7 @@ v 8.4.2 (unreleased) improvement when checking if a repository was empty - Add instrumentation for Gitlab::Git::Repository instance methods so we can track them in Performance Monitoring. + - Increase contrast between highlighted code comments and inline diff marker. v 8.4.2 (unreleased) - Fix method undefined when using external commit status in builds diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index 41a4579e1bc..b794da2ce98 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -22,11 +22,11 @@ // Diff line .line_holder { .diff-line-num.new, .line_content.new { - @include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.3), #808080); + @include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.2), #808080); } .diff-line-num.old, .line_content.old { - @include diff_background(rgba(255, 51, 51, 0.2), rgba(255, 51, 51, 0.3), #808080); + @include diff_background(rgba(255, 51, 51, 0.2), rgba(255, 51, 51, 0.25), #808080); } .line_content.match { diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index 22cee1af39f..9098e07adcd 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -22,11 +22,11 @@ // Diff line .line_holder { .diff-line-num.new, .line_content.new { - @include diff_background(rgba(166, 226, 46, 0.2), rgba(166, 226, 46, 0.3), #808080); + @include diff_background(rgba(166, 226, 46, 0.1), rgba(166, 226, 46, 0.15), #808080); } .diff-line-num.old, .line_content.old { - @include diff_background(rgba(254, 147, 140, 0.2), rgba(254, 147, 140, 0.3), #808080); + @include diff_background(rgba(254, 147, 140, 0.15), rgba(254, 147, 140, 0.2), #808080); } .line_content.match { diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index 0e8f30b4b0b..8b1a2824f76 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -22,11 +22,11 @@ // Diff line .line_holder { .diff-line-num.new, .line_content.new { - @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.3), #113b46); + @include diff_background(rgba(133, 153, 0, 0.15), rgba(133, 153, 0, 0.25), #113b46); } .diff-line-num.old, .line_content.old { - @include diff_background(rgba(220, 50, 47, 0.3), rgba(220, 50, 47, 0.3), #113b46); + @include diff_background(rgba(220, 50, 47, 0.3), rgba(220, 50, 47, 0.25), #113b46); } .line_content.match { diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index 08b6c835907..7ad89dd2c7c 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -22,11 +22,11 @@ // Diff line .line_holder { .diff-line-num.new, .line_content.new { - @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.3), #c5d0d4); + @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.25), #c5d0d4); } .diff-line-num.old, .line_content.old { - @include diff_background(rgba(220, 50, 47, 0.2), rgba(220, 50, 47, 0.3), #c5d0d4); + @include diff_background(rgba(220, 50, 47, 0.2), rgba(220, 50, 47, 0.25), #c5d0d4); } .line_content.match { -- cgit v1.2.1 From 4be80f8aa3c585882b4499d6d69dcbf4a97eff29 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 27 Jan 2016 16:15:24 +0100 Subject: Fix highlighting in blame view. --- CHANGELOG | 1 + app/controllers/projects/blame_controller.rb | 4 ++++ app/views/projects/blame/show.html.haml | 6 ++---- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4b283f235a4..aebd6643e8b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,6 +20,7 @@ v 8.4.2 (unreleased) improvement when checking if a repository was empty - Add instrumentation for Gitlab::Git::Repository instance methods so we can track them in Performance Monitoring. + - Fix highlighting in blame view. v 8.4.2 (unreleased) - Fix method undefined when using external commit status in builds diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb index 9ea518e6c85..d2aee3879e9 100644 --- a/app/controllers/projects/blame_controller.rb +++ b/app/controllers/projects/blame_controller.rb @@ -18,7 +18,10 @@ class Projects::BlameController < Projects::ApplicationController groups = [] current_group = nil + highlighted_lines = Gitlab::Highlight.highlight(@blob.name, @blob.data).lines + i = 0 blame.each do |commit, line| + line = highlighted_lines[i].html_safe if prev_sha && prev_sha == commit.sha current_group[:lines] << line else @@ -27,6 +30,7 @@ class Projects::BlameController < Projects::ApplicationController end prev_sha = commit.sha + i += 1 end groups << current_group if current_group.present? diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 53dcac78a9f..aecdec32d00 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -15,7 +15,6 @@ .file-content.blame.code.js-syntax-highlight %table - current_line = 1 - - blame_highlighter = highlighter(@blob.name, @blob.data, nowrap: true) - @blame.each do |blame_group| %tr %td.blame-commit @@ -38,8 +37,7 @@ \ - current_line += line_count %td.lines - %pre{class: 'code highlight'} + %pre.code.highlight %code - blame_group[:lines].each do |line| - :preserve - #{blame_highlighter.highlight(line)} + #{line} -- cgit v1.2.1 From e862af95375ed9e7ff778352c3900946bbb8e7e7 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 27 Jan 2016 16:16:03 +0100 Subject: merge spec and spinach jobs --- .gitlab-ci.yml | 101 +++------------------------------------------------------ 1 file changed, 4 insertions(+), 97 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 414344a7f75..dbdbae9d787 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -137,116 +137,23 @@ bundler:audit: # Ruby 2.1 jobs -spec:feature:ruby21: +spec:ruby21: image: ruby:2.1 script: - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature - tags: - - ruby - - mysql - only: - - master - -spec:api:ruby21: - image: ruby:2.1 - script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api - tags: - - ruby - - mysql - only: - - master - -spec:models:ruby21: - image: ruby:2.1 - script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models - tags: - - ruby - - mysql - only: - - master - -spec:lib:ruby21: - image: ruby:2.1 - script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib - tags: - - ruby - - mysql - only: - - master - -spec:services:ruby21: - image: ruby:2.1 - script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services - tags: - - ruby - - mysql - only: - - master - -spec:benchmark:ruby21: - image: ruby:2.1 - script: - - RAILS_ENV=test bundle exec rake spec:benchmark - tags: - - ruby - - mysql - allow_failure: true - only: - - master - -spec:other:ruby21: - image: ruby:2.1 - script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec tags: - ruby - mysql only: - master -spinach:project:half:ruby21: +spinach:ruby21: image: ruby:2.1 script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach tags: - ruby - mysql only: - master - -spinach:project:rest:ruby21: - image: ruby:2.1 - script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest - tags: - - ruby - - mysql - only: - - master - -spinach:other:ruby21: - image: ruby:2.1 - script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other - tags: - - ruby - - mysql - only: - - master - -bundler:audit:ruby21: - image: ruby:2.1 - script: - - "bundle exec bundle-audit update" - - "bundle exec bundle-audit check" - tags: - - ruby - - mysql - allow_failure: true - only: - - master -- cgit v1.2.1 From 47e0d6b16242cef440244e7d31f1310dea42f5ef Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 27 Jan 2016 16:19:16 +0100 Subject: updated README to include Ruby 2.1 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6ab3b1fcc5f..22dbf841bdc 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Instructions on how to start GitLab and how to run the tests can be found in the GitLab is a Ruby on Rails application that runs on the following software: - Ubuntu/Debian/CentOS/RHEL -- Ruby (MRI) 2.2 +- Ruby (MRI) 2.1 or 2.2 - Git 1.7.10+ - Redis 2.8+ - MySQL or PostgreSQL -- cgit v1.2.1 From 5cce1278adb168cd9fa4f189e3656a59726b4e6c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 27 Jan 2016 17:23:59 +0100 Subject: Correctly determine MR diff base when MR has merge conflicts --- CHANGELOG | 3 +-- app/controllers/projects/compare_controller.rb | 2 +- app/models/merge_request.rb | 4 ++-- app/models/merge_request_diff.rb | 28 ++++++++++---------------- app/models/project.rb | 4 ++++ 5 files changed, 19 insertions(+), 22 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4b283f235a4..e7273e017ea 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,8 +20,7 @@ v 8.4.2 (unreleased) improvement when checking if a repository was empty - Add instrumentation for Gitlab::Git::Repository instance methods so we can track them in Performance Monitoring. - -v 8.4.2 (unreleased) + - Correctly highlight MR diff when MR has merge conflicts - Fix method undefined when using external commit status in builds v 8.4.1 diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index f8ec76cd4e5..7bbe75b3974 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -21,7 +21,7 @@ class Projects::CompareController < Projects::ApplicationController @commits = Commit.decorate(compare_result.commits, @project) @diffs = compare_result.diffs @commit = @project.commit(head_ref) - @base_commit = @project.commit(base_ref) + @base_commit = @project.merge_base_commit(base_ref, head_ref) @diff_refs = [@base_commit, @commit] @line_notes = [] end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 41dd248d80a..85dce768cf3 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -184,7 +184,7 @@ class MergeRequest < ActiveRecord::Base if merge_request_diff merge_request_diff.base_commit else - self.target_project.commit(self.target_branch) + self.target_project.merge_base_commit(self.source_sha, self.target_branch) end end @@ -489,7 +489,7 @@ class MergeRequest < ActiveRecord::Base end def source_sha - commits.first.sha + last_commit.sha end def fetch_ref diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index ba0194cd0a6..c95179d6046 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -48,14 +48,11 @@ class MergeRequestDiff < ActiveRecord::Base end def diffs_no_whitespace - # Get latest sha of branch from source project - source_sha = merge_request.source_project.commit(source_branch).sha - compare_result = Gitlab::CompareResult.new( Gitlab::Git::Compare.new( - merge_request.target_project.repository.raw_repository, - merge_request.target_branch, - source_sha, + self.repository.raw_repository, + self.target_branch, + self.source_sha, ), { ignore_whitespace_change: true } ) @diffs_no_whitespace ||= load_diffs(dump_commits(compare_result.diffs)) @@ -83,8 +80,6 @@ class MergeRequestDiff < ActiveRecord::Base @last_commit_short_sha ||= last_commit.short_id end - private - def dump_commits(commits) commits.map(&:to_hash) end @@ -163,7 +158,7 @@ class MergeRequestDiff < ActiveRecord::Base self.st_diffs = new_diffs - self.base_commit_sha = merge_request.target_project.commit(target_branch).try(:sha) + self.base_commit_sha = self.repository.merge_base(self.source_sha, self.target_branch) self.save end @@ -181,7 +176,10 @@ class MergeRequestDiff < ActiveRecord::Base merge_request.target_project.repository end - private + def source_sha + source_commit = merge_request.source_project.commit(source_branch) + source_commit.try(:sha) + end def compare_result @compare_result ||= @@ -189,15 +187,11 @@ class MergeRequestDiff < ActiveRecord::Base # Update ref for merge request merge_request.fetch_ref - # Get latest sha of branch from source project - source_commit = merge_request.source_project.commit(source_branch) - source_sha = source_commit.try(:sha) - Gitlab::CompareResult.new( Gitlab::Git::Compare.new( - merge_request.target_project.repository.raw_repository, - merge_request.target_branch, - source_sha, + self.repository.raw_repository, + self.target_branch, + self.source_sha ) ) end diff --git a/app/models/project.rb b/app/models/project.rb index 4bd51449c25..488dc98c17f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -348,6 +348,10 @@ class Project < ActiveRecord::Base repository.commit(id) end + def merge_base_commit(first_commit_id, second_commit_id) + repository.commit(repository.merge_base(first_commit_id, second_commit_id)) + end + def saved? id && persisted? end -- cgit v1.2.1 From 0ee875f9618f3e14b52165f99dfdae21c0512d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 27 Jan 2016 15:21:27 -0500 Subject: Align filter dropdown. --- app/assets/stylesheets/pages/projects.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index d3d644329af..e4ea47cc4a2 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -574,6 +574,10 @@ pre.light-well { } .projects-search-form { + .dropdown-toggle.btn { + margin-top: -3px; + } + &.fork-search-form { margin: 0; margin-top: -$gl-padding; -- cgit v1.2.1 From 8c6231d1844c651eae31841e879a31a9c5d32cb8 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 27 Jan 2016 16:13:43 -0500 Subject: Update CHANGELOG [ci skip] --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index d78c38cf1dc..6727751dd63 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,8 @@ v 8.5.0 (unreleased) - Ignore binary files in code search to prevent Error 500 (Stan Hu) - Upgrade gitlab_git to 7.2.23 to fix commit message mentions in first branch push - New UI for pagination + - Don't prevent sign out when 2FA enforcement is enabled and user hasn't yet + set it up - Fix diff comments loaded by AJAX to load comment with diff in discussion tab - Whitelist raw "abbr" elements when parsing Markdown (Benedict Etzel) -- cgit v1.2.1 From 716892b4fdc93dbf21a1d0142444174d7415d747 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 27 Jan 2016 17:48:52 -0500 Subject: Update CHANGELOG [ci skip] --- CHANGELOG | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1c809254341..14f2f14becd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,7 +15,7 @@ v 8.5.0 (unreleased) - Fix visibility level text in admin area (Zeger-Jan van de Weg) - Update the ExternalIssue regex pattern (Blake Hitchcock) -v 8.4.2 (unreleased) +v 8.4.2 - Bump required gitlab-workhorse version to bring in a fix for missing artifacts in the build artifacts browser - Get rid of those ugly borders on the file tree view @@ -24,9 +24,7 @@ v 8.4.2 (unreleased) improvement when checking if a repository was empty - Add instrumentation for Gitlab::Git::Repository instance methods so we can track them in Performance Monitoring. - - Increase contrast between highlighted code comments and inline diff marker. - -v 8.4.2 (unreleased) + - Increase contrast between highlighted code comments and inline diff marker - Fix method undefined when using external commit status in builds v 8.4.1 -- cgit v1.2.1 From ca171b8190623023f3e6d00146abee8651cec857 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 27 Jan 2016 18:29:00 -0500 Subject: Update gitlab-workhorse versions in documentation [ci skip] --- doc/install/installation.md | 2 +- doc/update/8.3-to-8.4.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index f05097fedc5..2cc2dbef41b 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -355,7 +355,7 @@ GitLab Shell is an SSH access and repository management software developed speci cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git cd gitlab-workhorse - sudo -u git -H git checkout 0.5.4 + sudo -u git -H git checkout 0.6.2 sudo -u git -H make ### Initialize Database and Activate Advanced Features diff --git a/doc/update/8.3-to-8.4.md b/doc/update/8.3-to-8.4.md index 164cbcb53ce..616b1f58b65 100644 --- a/doc/update/8.3-to-8.4.md +++ b/doc/update/8.3-to-8.4.md @@ -48,7 +48,7 @@ which should already be on your system from GitLab 8.1. ```bash cd /home/git/gitlab-workhorse sudo -u git -H git fetch --all -sudo -u git -H git checkout 0.6.1 +sudo -u git -H git checkout 0.6.2 sudo -u git -H make ``` -- cgit v1.2.1 From 22e81f5dbe49a04d4df1fefe53a06dd561ffb6b9 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 28 Jan 2016 09:05:00 +0000 Subject: Removed sorted text from dropdowns Closes #12796 --- app/views/admin/groups/index.html.haml | 2 +- app/views/admin/projects/index.html.haml | 2 +- app/views/admin/users/index.html.haml | 2 +- app/views/explore/groups/index.html.haml | 2 +- app/views/explore/projects/_dropdown.html.haml | 3 +-- app/views/projects/branches/index.html.haml | 2 +- app/views/shared/_sort_dropdown.html.haml | 2 +- 7 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index 3940210e19b..118d3cfea07 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -17,7 +17,7 @@ .pull-right .dropdown.inline %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %span.light sort: + %span.light - if @sort.present? = sort_options_hash[@sort] - else diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index b96ad6e2208..d39c0f44031 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -50,7 +50,7 @@ .controls .dropdown.inline %button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'} - %span.light sort: + %span.light - if @sort.present? = sort_options_hash[@sort] - else diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index b050a4d01c3..b6b1168bd37 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -32,7 +32,7 @@ .pull-right .dropdown.inline %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %span.light sort: + %span.light - if @sort.present? = sort_options_hash[@sort] - else diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index fcb07b04083..8ffca96bb4e 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -18,7 +18,7 @@ .pull-right .dropdown.inline %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} - %span.light sort: + %span.light - if @sort.present? = sort_options_hash[@sort] - else diff --git a/app/views/explore/projects/_dropdown.html.haml b/app/views/explore/projects/_dropdown.html.haml index b23a3c1e5c1..a988d4c8154 100644 --- a/app/views/explore/projects/_dropdown.html.haml +++ b/app/views/explore/projects/_dropdown.html.haml @@ -1,6 +1,6 @@ .dropdown.inline %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} - %span.light sort: + %span.light - if @sort.present? = sort_options_hash[@sort] - elsif current_page?(trending_explore_projects_path) || current_page?(explore_root_path) @@ -24,4 +24,3 @@ = sort_title_recently_updated = link_to explore_projects_filter_path(sort: sort_value_oldest_updated) do = sort_title_oldest_updated - diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 204def60794..7afea5a5049 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -10,7 +10,7 @@   .dropdown.inline %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} - %span.light sort: + %span.light - if @sort.present? = @sort.humanize - else diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index af3d35de325..f09ab25276d 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -1,6 +1,6 @@ .dropdown.inline.prepend-left-10 %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} - %span.light sort: + %span.light - if @sort.present? = sort_options_hash[@sort] - else -- cgit v1.2.1 From 34a12e8cf992f3e4b4635ff0dabcce563e9c1a5e Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 28 Jan 2016 10:37:20 +0100 Subject: Update sentry-raven gem. --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index d2c4ddfba56..ec92964df25 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -725,7 +725,7 @@ GEM activesupport (>= 3.1, < 4.3) select2-rails (3.5.9.3) thor (~> 0.14) - sentry-raven (0.15.3) + sentry-raven (0.15.4) faraday (>= 0.7.6) settingslogic (2.0.9) sexp_processor (4.6.0) -- cgit v1.2.1 From 869b4d7c6ad5c025f310eeecce7293bf5bbc4926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 28 Jan 2016 10:14:21 +0100 Subject: Only create the defaults ApplicationSetting when DB is fully migrated Return a fake application settings OpenStruct when this is not the case. Also, use ActiveRecord::Base.connection_pool.active_connection? instead of ActiveRecord::Base.connection.active? to avoid driver exception. --- lib/gitlab/current_settings.rb | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 19b7427256c..e66e7768f2b 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -4,11 +4,14 @@ module Gitlab key = :current_application_settings RequestStore.store[key] ||= begin + settings = nil + if connect_to_db? - ApplicationSetting.current || ApplicationSetting.create_from_defaults - else - fake_application_settings + settings = ApplicationSetting.current + settings ||= ApplicationSetting.create_from_defaults unless ActiveRecord::Migrator.needs_migration? end + + settings || fake_application_settings end end @@ -18,35 +21,29 @@ module Gitlab 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'], restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], max_attachment_size: Settings.gitlab['max_attachment_size'], session_expire_delay: Settings.gitlab['session_expire_delay'], - import_sources: Settings.gitlab['import_sources'], + default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], + default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], + restricted_signup_domains: Settings.gitlab['restricted_signup_domains'], + import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], max_artifacts_size: Settings.artifacts['max_size'], + require_two_factor_authentication: false, + two_factor_grace_period: 48 ) end private def connect_to_db? - use_db = if ENV['USE_DB'] == "false" - false - else - true - end - - use_db && ActiveRecord::Base.connected? && - # The following condition is important: if a migrations adds a - # column to the application_settings table and a validation in - # the ApplicationSetting uses this new column we might end-up in - # a vicious circle where migration crash before being done. - # See https://gitlab.com/gitlab-org/gitlab-ce/issues/12606 for - # a thorough explanation. - !ActiveRecord::Migrator.needs_migration? && - ActiveRecord::Base.connection.table_exists?('application_settings') + ENV['USE_DB'] != 'false' && + ActiveRecord::Base.connection_pool.active_connection? && + ActiveRecord::Base.connection.table_exists?('application_settings') rescue ActiveRecord::NoDatabaseError false -- cgit v1.2.1 From 902baa2e1c68eca33cc832fe334fd149e3880ee4 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 28 Jan 2016 11:41:58 +0100 Subject: trick rubocop and temporarily add ruby 2.1 images for any branch --- .gitlab-ci.yml | 10 ++++++---- app/helpers/commits_helper.rb | 2 +- app/helpers/projects_helper.rb | 2 +- spec/models/concerns/case_sensitivity_spec.rb | 12 ++++++------ 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dbdbae9d787..99d81a6db50 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -145,8 +145,9 @@ spec:ruby21: tags: - ruby - mysql - only: - - master +# TODO: put this back after testing +# only: +# - master spinach:ruby21: image: ruby:2.1 @@ -155,5 +156,6 @@ spinach:ruby21: tags: - ruby - mysql - only: - - master +# TODO: put this back after testing +# only: +# - master diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 53f8f913b33..1d14ee52cfc 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -152,7 +152,7 @@ module CommitsHelper options = { class: "commit-#{options[:source]}-link has_tooltip", - data: { 'original-title': sanitize(source_email) } + data: { 'original-title'.to_sym => sanitize(source_email) } } if user.nil? diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index c5823e50096..f343d81a92a 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -40,7 +40,7 @@ module ProjectsHelper link_to(author_html, user_path(author), class: "author_link").html_safe else title = opts[:title].sub(":name", sanitize(author.name)) - link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { 'original-title': title, container: 'body' } ).html_safe + link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { 'original-title'.to_sym => title, container: 'body' } ).html_safe end end diff --git a/spec/models/concerns/case_sensitivity_spec.rb b/spec/models/concerns/case_sensitivity_spec.rb index 6535246bf2d..92fdc5cd65d 100644 --- a/spec/models/concerns/case_sensitivity_spec.rb +++ b/spec/models/concerns/case_sensitivity_spec.rb @@ -37,7 +37,7 @@ describe CaseSensitivity, models: true do with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar'). and_return(criteria) - expect(model.iwhere('foo.bar': 'bar')).to eq(criteria) + expect(model.iwhere('foo.bar'.to_sym => 'bar')).to eq(criteria) end end @@ -87,8 +87,8 @@ describe CaseSensitivity, models: true do with(%q{LOWER("foo"."baz") = LOWER(:value)}, value: 'baz'). and_return(final) - got = model.iwhere('foo.bar': 'bar', - 'foo.baz': 'baz') + got = model.iwhere('foo.bar'.to_sym => 'bar', + 'foo.baz'.to_sym => 'baz') expect(got).to eq(final) end @@ -127,7 +127,7 @@ describe CaseSensitivity, models: true do with(%q{`foo`.`bar` = :value}, value: 'bar'). and_return(criteria) - expect(model.iwhere('foo.bar': 'bar')). + expect(model.iwhere('foo.bar'.to_sym => 'bar')). to eq(criteria) end end @@ -178,8 +178,8 @@ describe CaseSensitivity, models: true do with(%q{`foo`.`baz` = :value}, value: 'baz'). and_return(final) - got = model.iwhere('foo.bar': 'bar', - 'foo.baz': 'baz') + got = model.iwhere('foo.bar'.to_sym => 'bar', + 'foo.baz'.to_sym => 'baz') expect(got).to eq(final) end -- cgit v1.2.1 From 55ab92c00bb79a951dea477d59e440c80dc96f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 28 Jan 2016 12:23:37 +0100 Subject: Use ActiveRecord::Base.connection.active? and rescue any exception in connect_to_db? This ensures that rake tasks that don't need a DB connection can be run without one. --- lib/gitlab/current_settings.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index e66e7768f2b..a6b2f14521c 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -41,8 +41,11 @@ module Gitlab private def connect_to_db? + # When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised + active_db_connection = ActiveRecord::Base.connection.active? rescue false + ENV['USE_DB'] != 'false' && - ActiveRecord::Base.connection_pool.active_connection? && + active_db_connection && ActiveRecord::Base.connection.table_exists?('application_settings') rescue ActiveRecord::NoDatabaseError -- cgit v1.2.1 From dfb8803c1cf5946d3eb7d0e70ba6fa8896f67a6f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 28 Jan 2016 14:26:02 +0100 Subject: Improve consistency and duplication for Merge Request API * Follow REST for merge request API route * Remove repeating comments API for MR Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 2 + doc/api/merge_requests.md | 74 +------ lib/api/merge_requests.rb | 351 ++++++++++++++++--------------- spec/requests/api/merge_requests_spec.rb | 62 +++--- 4 files changed, 221 insertions(+), 268 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 14f2f14becd..a3dfe1c4a6d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,8 @@ v 8.5.0 (unreleased) - Track project import failure - Fix visibility level text in admin area (Zeger-Jan van de Weg) - Update the ExternalIssue regex pattern (Blake Hitchcock) + - Deprecate API "merge_request/:merge_request_id/comments". Use "merge_requests/:merge_request_id/notes" instead + - Deprecate API "merge_request/:merge_request_id/...". Use "merge_requests/:merge_request_id/..." instead v 8.4.2 - Bump required gitlab-workhorse version to bring in a fix for missing diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 8bc0a67067a..85ed31320b9 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -60,7 +60,7 @@ Parameters: Shows information about a single merge request. ``` -GET /projects/:id/merge_request/:merge_request_id +GET /projects/:id/merge_requests/:merge_request_id ``` Parameters: @@ -105,7 +105,7 @@ Parameters: Get a list of merge request commits. ``` -GET /projects/:id/merge_request/:merge_request_id/commits +GET /projects/:id/merge_requests/:merge_request_id/commits ``` Parameters: @@ -142,7 +142,7 @@ Parameters: Shows information about the merge request including its files and changes. ``` -GET /projects/:id/merge_request/:merge_request_id/changes +GET /projects/:id/merge_requests/:merge_request_id/changes ``` Parameters: @@ -264,7 +264,7 @@ If an error occurs, an error number and a message explaining the reason is retur Updates an existing merge request. You can change the target branch, title, or even close the MR. ``` -PUT /projects/:id/merge_request/:merge_request_id +PUT /projects/:id/merge_requests/:merge_request_id ``` Parameters: @@ -323,7 +323,7 @@ If merge request is already merged or closed - you get 405 and error message 'Me If you don't have permissions to accept this merge request - you'll get a 401 ``` -PUT /projects/:id/merge_request/:merge_request_id/merge +PUT /projects/:id/merge_requests/:merge_request_id/merge ``` Parameters: @@ -373,7 +373,7 @@ If the merge request is already merged or closed - you get 405 and error message In case the merge request is not set to be merged when the build succeeds, you'll also get a 406 error. ``` -PUT /projects/:id/merge_request/:merge_request_id/cancel_merge_when_build_succeeds +PUT /projects/:id/merge_requests/:merge_request_id/cancel_merge_when_build_succeeds ``` Parameters: @@ -409,66 +409,6 @@ Parameters: } ``` -## Post comment to MR - -Adds a comment to a merge request. - -``` -POST /projects/:id/merge_request/:merge_request_id/comments -``` - -Parameters: - -- `id` (required) - The ID of a project -- `merge_request_id` (required) - ID of merge request -- `note` (required) - Text of comment - -```json -{ - "note": "text1" -} -``` - -## Get the comments on a MR - -Gets all the comments associated with a merge request. - -``` -GET /projects/:id/merge_request/:merge_request_id/comments -``` - -Parameters: - -- `id` (required) - The ID of a project -- `merge_request_id` (required) - ID of merge request - -```json -[ - { - "note": "this is the 1st comment on the 2merge merge request", - "author": { - "id": 11, - "username": "admin", - "email": "admin@example.com", - "name": "Administrator", - "state": "active", - "created_at": "2014-03-06T08:17:35.000Z" - } - }, - { - "note": "Status changed to closed", - "author": { - "id": 11, - "username": "admin", - "email": "admin@example.com", - "name": "Administrator", - "state": "active", - "created_at": "2014-03-06T08:17:35.000Z" - } - } -] -``` - ## Comments on merge requets -Comments are done via the notes resource. +Comments are done via the [notes](notes.md) resource. diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 5c97fe1c88c..dd7f24f3279 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -59,55 +59,6 @@ module API present paginate(merge_requests), with: Entities::MergeRequest end - # Show MR - # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - The ID of MR - # - # Example: - # GET /projects/:id/merge_request/:merge_request_id - # - get ":id/merge_request/:merge_request_id" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) - - authorize! :read_merge_request, merge_request - - present merge_request, with: Entities::MergeRequest - end - - # Show MR commits - # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - The ID of MR - # - # Example: - # GET /projects/:id/merge_request/:merge_request_id/commits - # - get ':id/merge_request/:merge_request_id/commits' do - merge_request = user_project.merge_requests. - find(params[:merge_request_id]) - authorize! :read_merge_request, merge_request - present merge_request.commits, with: Entities::RepoCommit - end - - # Show MR changes - # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - The ID of MR - # - # Example: - # GET /projects/:id/merge_request/:merge_request_id/changes - # - get ':id/merge_request/:merge_request_id/changes' do - merge_request = user_project.merge_requests. - find(params[:merge_request_id]) - authorize! :read_merge_request, merge_request - present merge_request, with: Entities::MergeRequestChanges - end - # Create MR # # Parameters: @@ -148,146 +99,206 @@ module API end end - # Update MR + # Routing "merge_request/:merge_request_id/..." is DEPRECATED and WILL BE REMOVED in version 9.0 + # Use "merge_requests/:merge_request_id/..." instead. # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - ID of MR - # target_branch - The target branch - # assignee_id - Assignee user ID - # title - Title of MR - # state_event - Status of MR. (close|reopen|merge) - # description - Description of MR - # labels (optional) - Labels for a MR as a comma-separated list - # Example: - # PUT /projects/:id/merge_request/:merge_request_id - # - put ":id/merge_request/:merge_request_id" do - attrs = attributes_for_keys [:target_branch, :assignee_id, :title, :state_event, :description] - merge_request = user_project.merge_requests.find(params[:merge_request_id]) - authorize! :update_merge_request, merge_request - - # Ensure source_branch is not specified - if params[:source_branch].present? - render_api_error!('Source branch cannot be changed', 400) - end - - # Validate label names in advance - if (errors = validate_label_params(params)).any? - render_api_error!({ labels: errors }, 400) - end - - merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, attrs).execute(merge_request) - - if merge_request.valid? - # Find or create labels and attach to issue - unless params[:labels].nil? - merge_request.remove_labels - merge_request.add_labels_by_names(params[:labels].split(",")) - end + [":id/merge_request/:merge_request_id", ":id/merge_requests/:merge_request_id"].each do |path| + # Show MR + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - The ID of MR + # + # Example: + # GET /projects/:id/merge_requests/:merge_request_id + # + get path do + merge_request = user_project.merge_requests.find(params[:merge_request_id]) + + authorize! :read_merge_request, merge_request present merge_request, with: Entities::MergeRequest - else - handle_merge_request_errors! merge_request.errors end - end - - # Merge MR - # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - ID of MR - # merge_commit_message (optional) - Custom merge commit message - # should_remove_source_branch (optional) - When true, the source branch will be deleted if possible - # merge_when_build_succeeds (optional) - When true, this MR will be merged when the build succeeds - # Example: - # PUT /projects/:id/merge_request/:merge_request_id/merge - # - put ":id/merge_request/:merge_request_id/merge" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) - - # Merge request can not be merged - # because user dont have permissions to push into target branch - unauthorized! unless merge_request.can_be_merged_by?(current_user) - not_allowed! if !merge_request.open? || merge_request.work_in_progress? - merge_request.check_if_can_be_merged - - render_api_error!('Branch cannot be merged', 406) unless merge_request.can_be_merged? - - merge_params = { - commit_message: params[:merge_commit_message], - should_remove_source_branch: params[:should_remove_source_branch] - } - - if parse_boolean(params[:merge_when_build_succeeds]) && merge_request.ci_commit && merge_request.ci_commit.active? - ::MergeRequests::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user, merge_params). - execute(merge_request) - else - ::MergeRequests::MergeService.new(merge_request.target_project, current_user, merge_params). - execute(merge_request) + # Show MR commits + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - The ID of MR + # + # Example: + # GET /projects/:id/merge_requests/:merge_request_id/commits + # + get "#{path}/commits" do + merge_request = user_project.merge_requests. + find(params[:merge_request_id]) + authorize! :read_merge_request, merge_request + present merge_request.commits, with: Entities::RepoCommit end - present merge_request, with: Entities::MergeRequest - end + # Show MR changes + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - The ID of MR + # + # Example: + # GET /projects/:id/merge_requests/:merge_request_id/changes + # + get "#{path}/changes" do + merge_request = user_project.merge_requests. + find(params[:merge_request_id]) + authorize! :read_merge_request, merge_request + present merge_request, with: Entities::MergeRequestChanges + end - # Cancel Merge if Merge When build succeeds is enabled - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - ID of MR - # - post ":id/merge_request/:merge_request_id/cancel_merge_when_build_succeeds" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) + # Update MR + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - ID of MR + # target_branch - The target branch + # assignee_id - Assignee user ID + # title - Title of MR + # state_event - Status of MR. (close|reopen|merge) + # description - Description of MR + # labels (optional) - Labels for a MR as a comma-separated list + # Example: + # PUT /projects/:id/merge_requests/:merge_request_id + # + put path do + attrs = attributes_for_keys [:target_branch, :assignee_id, :title, :state_event, :description] + merge_request = user_project.merge_requests.find(params[:merge_request_id]) + authorize! :update_merge_request, merge_request + + # Ensure source_branch is not specified + if params[:source_branch].present? + render_api_error!('Source branch cannot be changed', 400) + end - unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user) + # Validate label names in advance + if (errors = validate_label_params(params)).any? + render_api_error!({ labels: errors }, 400) + end - ::MergeRequest::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user).cancel(merge_request) - end + merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, attrs).execute(merge_request) - # Get a merge request's comments - # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - ID of MR - # Examples: - # GET /projects/:id/merge_request/:merge_request_id/comments - # - get ":id/merge_request/:merge_request_id/comments" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) + if merge_request.valid? + # Find or create labels and attach to issue + unless params[:labels].nil? + merge_request.remove_labels + merge_request.add_labels_by_names(params[:labels].split(",")) + end - authorize! :read_merge_request, merge_request + present merge_request, with: Entities::MergeRequest + else + handle_merge_request_errors! merge_request.errors + end + end - present paginate(merge_request.notes.fresh), with: Entities::MRNote - end + # Merge MR + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - ID of MR + # merge_commit_message (optional) - Custom merge commit message + # should_remove_source_branch (optional) - When true, the source branch will be deleted if possible + # merge_when_build_succeeds (optional) - When true, this MR will be merged when the build succeeds + # Example: + # PUT /projects/:id/merge_requests/:merge_request_id/merge + # + put "#{path}/merge" do + merge_request = user_project.merge_requests.find(params[:merge_request_id]) + + # Merge request can not be merged + # because user dont have permissions to push into target branch + unauthorized! unless merge_request.can_be_merged_by?(current_user) + not_allowed! if !merge_request.open? || merge_request.work_in_progress? + + merge_request.check_if_can_be_merged + + render_api_error!('Branch cannot be merged', 406) unless merge_request.can_be_merged? + + merge_params = { + commit_message: params[:merge_commit_message], + should_remove_source_branch: params[:should_remove_source_branch] + } + + if parse_boolean(params[:merge_when_build_succeeds]) && merge_request.ci_commit && merge_request.ci_commit.active? + ::MergeRequests::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user, merge_params). + execute(merge_request) + else + ::MergeRequests::MergeService.new(merge_request.target_project, current_user, merge_params). + execute(merge_request) + end - # Post comment to merge request - # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - ID of MR - # note (required) - Text of comment - # Examples: - # POST /projects/:id/merge_request/:merge_request_id/comments - # - post ":id/merge_request/:merge_request_id/comments" do - required_attributes! [:note] + present merge_request, with: Entities::MergeRequest + end - merge_request = user_project.merge_requests.find(params[:merge_request_id]) + # Cancel Merge if Merge When build succeeds is enabled + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - ID of MR + # + post "#{path}/cancel_merge_when_build_succeeds" do + merge_request = user_project.merge_requests.find(params[:merge_request_id]) - authorize! :create_note, merge_request + unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user) - opts = { - note: params[:note], - noteable_type: 'MergeRequest', - noteable_id: merge_request.id - } + ::MergeRequest::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user).cancel(merge_request) + end - note = ::Notes::CreateService.new(user_project, current_user, opts).execute + # Duplicate. DEPRECATED and WILL BE REMOVED in 9.0. + # Use GET "/projects/:id/merge_requests/:merge_request_id/notes" instead + # + # Get a merge request's comments + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - ID of MR + # Examples: + # GET /projects/:id/merge_requests/:merge_request_id/comments + # + get "#{path}/comments" do + merge_request = user_project.merge_requests.find(params[:merge_request_id]) + + authorize! :read_merge_request, merge_request + + present paginate(merge_request.notes.fresh), with: Entities::MRNote + end - if note.save - present note, with: Entities::MRNote - else - render_api_error!("Failed to save note #{note.errors.messages}", 400) + # Duplicate. DEPRECATED and WILL BE REMOVED in 9.0. + # Use POST "/projects/:id/merge_requests/:merge_request_id/notes" instead + # + # Post comment to merge request + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - ID of MR + # note (required) - Text of comment + # Examples: + # POST /projects/:id/merge_requests/:merge_request_id/comments + # + post "#{path}/comments" do + required_attributes! [:note] + + merge_request = user_project.merge_requests.find(params[:merge_request_id]) + + authorize! :create_note, merge_request + + opts = { + note: params[:note], + noteable_type: 'MergeRequest', + noteable_id: merge_request.id + } + + note = ::Notes::CreateService.new(user_project, current_user, opts).execute + + if note.save + present note, with: Entities::MRNote + else + render_api_error!("Failed to save note #{note.errors.messages}", 400) + end end end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index e194eb93cf4..d7bfa17b0b1 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -109,9 +109,9 @@ describe API::API, api: true do end end - describe "GET /projects/:id/merge_request/:merge_request_id" do + describe "GET /projects/:id/merge_requests/:merge_request_id" do it "should return merge_request" do - get api("/projects/#{project.id}/merge_request/#{merge_request.id}", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) expect(response.status).to eq(200) expect(json_response['title']).to eq(merge_request.title) expect(json_response['iid']).to eq(merge_request.iid) @@ -126,14 +126,14 @@ describe API::API, api: true do end it "should return a 404 error if merge_request_id not found" do - get api("/projects/#{project.id}/merge_request/999", user) + get api("/projects/#{project.id}/merge_requests/999", user) expect(response.status).to eq(404) end end - describe 'GET /projects/:id/merge_request/:merge_request_id/commits' do + describe 'GET /projects/:id/merge_requests/:merge_request_id/commits' do context 'valid merge request' do - before { get api("/projects/#{project.id}/merge_request/#{merge_request.id}/commits", user) } + before { get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user) } let(:commit) { merge_request.commits.first } it { expect(response.status).to eq 200 } @@ -143,20 +143,20 @@ describe API::API, api: true do end it 'returns a 404 when merge_request_id not found' do - get api("/projects/#{project.id}/merge_request/999/commits", user) + get api("/projects/#{project.id}/merge_requests/999/commits", user) expect(response.status).to eq(404) end end - describe 'GET /projects/:id/merge_request/:merge_request_id/changes' do + describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do it 'should return the change information of the merge_request' do - get api("/projects/#{project.id}/merge_request/#{merge_request.id}/changes", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user) expect(response.status).to eq 200 expect(json_response['changes'].size).to eq(merge_request.diffs.size) end it 'returns a 404 when merge_request_id not found' do - get api("/projects/#{project.id}/merge_request/999/changes", user) + get api("/projects/#{project.id}/merge_requests/999/changes", user) expect(response.status).to eq(404) end end @@ -311,19 +311,19 @@ describe API::API, api: true do end end - describe "PUT /projects/:id/merge_request/:merge_request_id to close MR" do + describe "PUT /projects/:id/merge_requests/:merge_request_id to close MR" do it "should return merge_request" do - put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), state_event: "close" + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close" expect(response.status).to eq(200) expect(json_response['state']).to eq('closed') end end - describe "PUT /projects/:id/merge_request/:merge_request_id/merge" do + describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do let(:ci_commit) { create(:ci_commit_without_jobs) } it "should return merge_request in case of success" do - put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) expect(response.status).to eq(200) end @@ -332,7 +332,7 @@ describe API::API, api: true do allow_any_instance_of(MergeRequest). to receive(:can_be_merged?).and_return(false) - put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) expect(response.status).to eq(406) expect(json_response['message']).to eq('Branch cannot be merged') @@ -340,14 +340,14 @@ describe API::API, api: true do it "should return 405 if merge_request is not open" do merge_request.close - put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) expect(response.status).to eq(405) expect(json_response['message']).to eq('405 Method Not Allowed') end it "should return 405 if merge_request is a work in progress" do merge_request.update_attribute(:title, "WIP: #{merge_request.title}") - put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) expect(response.status).to eq(405) expect(json_response['message']).to eq('405 Method Not Allowed') end @@ -355,7 +355,7 @@ describe API::API, api: true do it "should return 401 if user has no permissions to merge" do user2 = create(:user) project.team << [user2, :reporter] - put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user2) + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2) expect(response.status).to eq(401) expect(json_response['message']).to eq('401 Unauthorized') end @@ -364,7 +364,7 @@ describe API::API, api: true do allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) allow(ci_commit).to receive(:active?).and_return(true) - put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user), merge_when_build_succeeds: true + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true expect(response.status).to eq(200) expect(json_response['title']).to eq('Test') @@ -372,33 +372,33 @@ describe API::API, api: true do end end - describe "PUT /projects/:id/merge_request/:merge_request_id" do + describe "PUT /projects/:id/merge_requests/:merge_request_id" do it "should return merge_request" do - put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), title: "New title" + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title" expect(response.status).to eq(200) expect(json_response['title']).to eq('New title') end it "should return merge_request" do - put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), description: "New description" + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), description: "New description" expect(response.status).to eq(200) expect(json_response['description']).to eq('New description') end it "should return 400 when source_branch is specified" do - put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), source_branch: "master", target_branch: "master" expect(response.status).to eq(400) end it "should return merge_request with renamed target_branch" do - put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), target_branch: "wiki" + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki" expect(response.status).to eq(200) expect(json_response['target_branch']).to eq('wiki') end it 'should return 400 on invalid label names' do - put api("/projects/#{project.id}/merge_request/#{merge_request.id}", + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: 'new issue', labels: 'label, ?' @@ -407,11 +407,11 @@ describe API::API, api: true do end end - describe "POST /projects/:id/merge_request/:merge_request_id/comments" do + describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do it "should return comment" do original_count = merge_request.notes.size - post api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user), note: "My comment" + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment" expect(response.status).to eq(201) expect(json_response['note']).to eq('My comment') expect(json_response['author']['name']).to eq(user.name) @@ -420,20 +420,20 @@ describe API::API, api: true do end it "should return 400 if note is missing" do - post api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user) + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) expect(response.status).to eq(400) end it "should return 404 if note is attached to non existent merge request" do - post api("/projects/#{project.id}/merge_request/404/comments", user), + post api("/projects/#{project.id}/merge_requests/404/comments", user), note: 'My comment' expect(response.status).to eq(404) end end - describe "GET :id/merge_request/:merge_request_id/comments" do + describe "GET :id/merge_requests/:merge_request_id/comments" do it "should return merge_request comments ordered by created_at" do - get api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.length).to eq(2) @@ -443,7 +443,7 @@ describe API::API, api: true do end it "should return a 404 error if merge_request_id not found" do - get api("/projects/#{project.id}/merge_request/999/comments", user) + get api("/projects/#{project.id}/merge_requests/999/comments", user) expect(response.status).to eq(404) end end -- cgit v1.2.1 From b636f83e5910d10a954464370efe42cc14a097ba Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 28 Jan 2016 14:49:21 +0100 Subject: Move blame group logic to dedicated class --- app/controllers/projects/blame_controller.rb | 28 +-------------- app/views/projects/blame/show.html.haml | 4 +-- lib/gitlab/blame.rb | 54 ++++++++++++++++++++++++++++ spec/controllers/blame_controller_spec.rb | 14 -------- spec/lib/gitlab/blame_spec.rb | 24 +++++++++++++ 5 files changed, 81 insertions(+), 43 deletions(-) create mode 100644 lib/gitlab/blame.rb create mode 100644 spec/lib/gitlab/blame_spec.rb diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb index d2aee3879e9..f576d0be1fc 100644 --- a/app/controllers/projects/blame_controller.rb +++ b/app/controllers/projects/blame_controller.rb @@ -8,32 +8,6 @@ class Projects::BlameController < Projects::ApplicationController def show @blob = @repository.blob_at(@commit.id, @path) - @blame = group_blame_lines - end - - def group_blame_lines - blame = Gitlab::Git::Blame.new(@repository, @commit.id, @path) - - prev_sha = nil - groups = [] - current_group = nil - - highlighted_lines = Gitlab::Highlight.highlight(@blob.name, @blob.data).lines - i = 0 - blame.each do |commit, line| - line = highlighted_lines[i].html_safe - if prev_sha && prev_sha == commit.sha - current_group[:lines] << line - else - groups << current_group if current_group.present? - current_group = { commit: commit, lines: [line] } - end - - prev_sha = commit.sha - i += 1 - end - - groups << current_group if current_group.present? - groups + @blame_groups = Gitlab::Blame.new(@blob, @commit).groups end end diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index aecdec32d00..eb6fbfaffa0 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -15,11 +15,11 @@ .file-content.blame.code.js-syntax-highlight %table - current_line = 1 - - @blame.each do |blame_group| + - @blame_groups.each do |blame_group| %tr %td.blame-commit .commit - - commit = Commit.new(blame_group[:commit], @project) + - commit = blame_group[:commit] .commit-row-title %strong = link_to_gfm truncate(commit.title, length: 35), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "cdark" diff --git a/lib/gitlab/blame.rb b/lib/gitlab/blame.rb new file mode 100644 index 00000000000..313e6b9fc03 --- /dev/null +++ b/lib/gitlab/blame.rb @@ -0,0 +1,54 @@ +module Gitlab + class Blame + attr_accessor :blob, :commit + + def initialize(blob, commit) + @blob = blob + @commit = commit + end + + def groups(highlight: true) + prev_sha = nil + groups = [] + current_group = nil + + i = 0 + blame.each do |commit, line| + commit = Commit.new(commit, project) + + sha = commit.sha + if prev_sha != sha + groups << current_group if current_group + current_group = { commit: commit, lines: [] } + end + + line = highlighted_lines[i].html_safe if highlight + current_group[:lines] << line + + prev_sha = sha + i += 1 + end + groups << current_group if current_group + + groups + end + + private + + def blame + @blame ||= Gitlab::Git::Blame.new(repository, @commit.id, @blob.path) + end + + def highlighted_lines + @highlighted_lines ||= Gitlab::Highlight.highlight(@blob.name, @blob.data).lines + end + + def project + commit.project + end + + def repository + project.repository + end + end +end diff --git a/spec/controllers/blame_controller_spec.rb b/spec/controllers/blame_controller_spec.rb index 3ad4d5fc0a8..25f06299a29 100644 --- a/spec/controllers/blame_controller_spec.rb +++ b/spec/controllers/blame_controller_spec.rb @@ -24,20 +24,6 @@ describe Projects::BlameController do context "valid file" do let(:id) { 'master/files/ruby/popen.rb' } it { is_expected.to respond_with(:success) } - - it 'groups blames properly' do - blame = assigns(:blame) - # Sanity check a few items - expect(blame.count).to eq(18) - expect(blame[0][:commit].sha).to eq('913c66a37b4a45b9769037c55c2d238bd0942d2e') - expect(blame[0][:lines]).to eq(["require 'fileutils'", "require 'open3'", ""]) - - expect(blame[1][:commit].sha).to eq('874797c3a73b60d2187ed6e2fcabd289ff75171e') - expect(blame[1][:lines]).to eq(["module Popen", " extend self"]) - - expect(blame[-1][:commit].sha).to eq('913c66a37b4a45b9769037c55c2d238bd0942d2e') - expect(blame[-1][:lines]).to eq([" end", "end"]) - end end end end diff --git a/spec/lib/gitlab/blame_spec.rb b/spec/lib/gitlab/blame_spec.rb new file mode 100644 index 00000000000..89245761b6f --- /dev/null +++ b/spec/lib/gitlab/blame_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Gitlab::Blame, lib: true do + let(:project) { create(:project) } + let(:path) { 'files/ruby/popen.rb' } + let(:commit) { project.commit('master') } + let(:blob) { project.repository.blob_at(commit.id, path) } + + describe "#groups" do + let(:subject) { described_class.new(blob, commit).groups(highlight: false) } + + it 'groups lines properly' do + expect(subject.count).to eq(18) + expect(subject[0][:commit].sha).to eq('913c66a37b4a45b9769037c55c2d238bd0942d2e') + expect(subject[0][:lines]).to eq(["require 'fileutils'", "require 'open3'", ""]) + + expect(subject[1][:commit].sha).to eq('874797c3a73b60d2187ed6e2fcabd289ff75171e') + expect(subject[1][:lines]).to eq(["module Popen", " extend self"]) + + expect(subject[-1][:commit].sha).to eq('913c66a37b4a45b9769037c55c2d238bd0942d2e') + expect(subject[-1][:lines]).to eq([" end", "end"]) + end + end +end -- cgit v1.2.1 From 7d07091c6cedb0386017a9f360c971973ac7a920 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 28 Jan 2016 14:49:53 +0100 Subject: Fix changelog --- CHANGELOG | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 701574c893a..fcc5706168a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -24,11 +24,9 @@ v 8.4.2 improvement when checking if a repository was empty - Add instrumentation for Gitlab::Git::Repository instance methods so we can track them in Performance Monitoring. - - Fix highlighting in blame view. - -v 8.4.2 (unreleased) - Increase contrast between highlighted code comments and inline diff marker - Fix method undefined when using external commit status in builds + - Fix highlighting in blame view. v 8.4.1 - Apply security updates for Rails (4.2.5.1), rails-html-sanitizer (1.0.3), -- cgit v1.2.1 From 30b0d06e9f74f0068926314ec03b003fbd86c8f2 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 28 Jan 2016 15:10:48 +0100 Subject: Fix specs --- app/models/merge_request.rb | 4 ++-- app/models/project.rb | 3 ++- app/models/repository.rb | 2 ++ spec/models/build_spec.rb | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 85dce768cf3..0af60645545 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -183,7 +183,7 @@ class MergeRequest < ActiveRecord::Base def diff_base_commit if merge_request_diff merge_request_diff.base_commit - else + elsif source_sha self.target_project.merge_base_commit(self.source_sha, self.target_branch) end end @@ -489,7 +489,7 @@ class MergeRequest < ActiveRecord::Base end def source_sha - last_commit.sha + last_commit.try(:sha) end def fetch_ref diff --git a/app/models/project.rb b/app/models/project.rb index 488dc98c17f..238932f59a7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -349,7 +349,8 @@ class Project < ActiveRecord::Base end def merge_base_commit(first_commit_id, second_commit_id) - repository.commit(repository.merge_base(first_commit_id, second_commit_id)) + sha = repository.merge_base(first_commit_id, second_commit_id) + repository.commit(sha) if sha end def saved? diff --git a/app/models/repository.rb b/app/models/repository.rb index 6c1ee4b29cd..130daddd9d1 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -589,6 +589,8 @@ class Repository def merge_base(first_commit_id, second_commit_id) rugged.merge_base(first_commit_id, second_commit_id) + rescue Rugged::ReferenceError + nil end def is_ancestor?(ancestor_id, descendant_id) diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index d30bc7d0554..606340d87e4 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Ci::Build, models: true do - let(:project) { FactoryGirl.create :empty_project } + let(:project) { FactoryGirl.create :project } let(:commit) { FactoryGirl.create :ci_commit, project: project } let(:build) { FactoryGirl.create :ci_build, commit: commit } -- cgit v1.2.1 From f9218898ece79275c585da3aeb932b7ede409776 Mon Sep 17 00:00:00 2001 From: Josh Frye Date: Wed, 27 Jan 2016 10:59:16 -0500 Subject: [WIP] Background process note logic for #3948 --- app/controllers/projects/notes_controller.rb | 2 +- app/services/notes/create_service.rb | 19 ++---------------- app/services/notes/post_process_service.rb | 30 ++++++++++++++++++++++++++++ app/workers/new_note_worker.rb | 12 +++++++++++ 4 files changed, 45 insertions(+), 18 deletions(-) create mode 100644 app/services/notes/post_process_service.rb create mode 100644 app/workers/new_note_worker.rb diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 4a2599dda37..1b9dd568043 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -106,7 +106,7 @@ class Projects::NotesController < Projects::ApplicationController { notes_left: [note], notes_right: [] } else { notes_left: [], notes_right: [note] } - end + end else template = "projects/notes/_diff_notes_with_reply" locals = { notes: [note] } diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index a8486e6a5a1..8d9661167b5 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -6,27 +6,12 @@ module Notes note.system = false if note.save - notification_service.new_note(note) - - # Skip system notes, like status changes and cross-references and awards - unless note.system || note.is_award - event_service.leave_note(note, note.author) - note.create_cross_references! - execute_hooks(note) - end + # Finish the harder work in the background + NewNoteWorker.perform_in(2.seconds, note.id, params) end note end - def hook_data(note) - Gitlab::NoteDataBuilder.build(note, current_user) - end - - def execute_hooks(note) - note_data = hook_data(note) - note.project.execute_hooks(note_data, :note_hooks) - note.project.execute_services(note_data, :note_hooks) - end end end diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb new file mode 100644 index 00000000000..f37d3c50cdd --- /dev/null +++ b/app/services/notes/post_process_service.rb @@ -0,0 +1,30 @@ +module Notes + class PostProcessService + + attr_accessor :note + + def initialize(note) + @note = note + end + + def execute + # Skip system notes, like status changes and cross-references and awards + unless @note.system || @note.is_award + EventCreateService.new.leave_note(@note, @note.author) + @note.create_cross_references! + execute_note_hooks + end + end + + def hook_data + Gitlab::NoteDataBuilder.build(@note, @note.author) + end + + def execute_note_hooks + note_data = hook_data + @note.project.execute_hooks(note_data, :note_hooks) + @note.project.execute_services(note_data, :note_hooks) + end + + end +end diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb new file mode 100644 index 00000000000..1b3232cd365 --- /dev/null +++ b/app/workers/new_note_worker.rb @@ -0,0 +1,12 @@ +class NewNoteWorker + include Sidekiq::Worker + + sidekiq_options queue: :default + + def perform(note_id, note_params) + note = Note.find(note_id) + + NotificationService.new.new_note(note) + Notes::PostProcessService.new(note).execute + end +end -- cgit v1.2.1 From c95bc65519993f15fe0fca52db7c315f18053b55 Mon Sep 17 00:00:00 2001 From: Josh Frye Date: Thu, 28 Jan 2016 13:04:24 -0500 Subject: Update tests --- spec/services/notes/create_service_spec.rb | 4 +--- spec/services/notes/post_process_service_spec.rb | 26 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 spec/services/notes/post_process_service_spec.rb diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index a797a2fe4aa..ff23f13e1cb 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -14,9 +14,7 @@ describe Notes::CreateService, services: true do noteable_type: 'Issue', noteable_id: issue.id } - - expect(project).to receive(:execute_hooks) - expect(project).to receive(:execute_services) + @note = Notes::CreateService.new(project, user, opts).execute end diff --git a/spec/services/notes/post_process_service_spec.rb b/spec/services/notes/post_process_service_spec.rb new file mode 100644 index 00000000000..6b632f94b90 --- /dev/null +++ b/spec/services/notes/post_process_service_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Notes::CreateService, services: true do + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + let(:user) { create(:user) } + + describe :execute do + before do + project.team << [user, :master] + note_opts = { + note: 'Awesome comment', + noteable_type: 'Issue', + noteable_id: issue.id + } + + @note = Notes::CreateService.new(project, user, note_opts).execute + end + + it { + expect(project).to receive(:execute_hooks) + expect(project).to receive(:execute_services) + Notes::PostProcessService.new(@note).execute + } + end +end -- cgit v1.2.1 From 1e666ce6958b2b911276d533a9efd699884f6a71 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 28 Jan 2016 13:31:48 -0500 Subject: Backport LDAP user assignment changes from EE See https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/146 --- app/controllers/omniauth_callbacks_controller.rb | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 4cad98b8e98..4c13228fce9 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -21,15 +21,16 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController # We only find ourselves here # if the authentication to LDAP was successful. def ldap - @user = Gitlab::LDAP::User.new(oauth) - @user.save if @user.changed? # will also save new users - gl_user = @user.gl_user - gl_user.remember_me = params[:remember_me] if @user.persisted? + ldap_user = Gitlab::LDAP::User.new(oauth) + ldap_user.save if ldap_user.changed? # will also save new users + + @user = ldap_user.gl_user + @user.remember_me = params[:remember_me] if ldap_user.persisted? # Do additional LDAP checks for the user filter and EE features - if @user.allowed? - log_audit_event(gl_user, with: :ldap) - sign_in_and_redirect(gl_user) + if ldap_user.allowed? + log_audit_event(@user, with: :ldap) + sign_in_and_redirect(@user) else flash[:alert] = "Access denied for your LDAP account." redirect_to new_user_session_path -- cgit v1.2.1 From 79123ec9986c8638b569daa02ac2ba2f3408c48e Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 28 Jan 2016 14:29:09 -0500 Subject: Revert "Revert "Remove the `:coffee` and `:coffeescript` Haml filters"" This reverts commit b0145d765b94d2cef86b10b6dd22895779cfef33. --- config/initializers/haml.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/initializers/haml.rb b/config/initializers/haml.rb index 7e8ddb3716b..1516476815a 100644 --- a/config/initializers/haml.rb +++ b/config/initializers/haml.rb @@ -1 +1,7 @@ Haml::Template.options[:ugly] = true + +# Remove the `:coffee` and `:coffeescript` filters +# +# See https://git.io/vztMu and http://stackoverflow.com/a/17571242/223897 +Haml::Filters.remove_filter('coffee') +Haml::Filters.remove_filter('coffeescript') -- cgit v1.2.1 From f3d4f2eac7bcf787a00a6cff694fbfa8969830f4 Mon Sep 17 00:00:00 2001 From: Josh Frye Date: Thu, 28 Jan 2016 13:23:37 -0500 Subject: Fix Rubocop error --- spec/services/notes/post_process_service_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/services/notes/post_process_service_spec.rb b/spec/services/notes/post_process_service_spec.rb index 6b632f94b90..1a3f339bd64 100644 --- a/spec/services/notes/post_process_service_spec.rb +++ b/spec/services/notes/post_process_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Notes::CreateService, services: true do +describe Notes::PostProcessService, services: true do let(:project) { create(:empty_project) } let(:issue) { create(:issue, project: project) } let(:user) { create(:user) } @@ -17,10 +17,10 @@ describe Notes::CreateService, services: true do @note = Notes::CreateService.new(project, user, note_opts).execute end - it { + it do expect(project).to receive(:execute_hooks) expect(project).to receive(:execute_services) Notes::PostProcessService.new(@note).execute - } + end end end -- cgit v1.2.1 From 6e4d36b49d7fc9c41e136f234c6b5dd74a86df50 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 28 Jan 2016 21:54:45 +0100 Subject: add back master only for ruby 2.1 images --- .gitlab-ci.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 99d81a6db50..dbdbae9d787 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -145,9 +145,8 @@ spec:ruby21: tags: - ruby - mysql -# TODO: put this back after testing -# only: -# - master + only: + - master spinach:ruby21: image: ruby:2.1 @@ -156,6 +155,5 @@ spinach:ruby21: tags: - ruby - mysql -# TODO: put this back after testing -# only: -# - master + only: + - master -- cgit v1.2.1 From ca05054ea2a8aff81822f310c5dafb68ae26e10e Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 28 Jan 2016 16:28:19 -0500 Subject: Partially revert "Add IP check against DNSBLs at account sign-up" This partially reverts 6a5cd3ca - we keep the migration and add a new migration that reverts it in order to keep migration history intact. --- CHANGELOG | 1 + .../admin/application_settings_controller.rb | 2 - app/controllers/registrations_controller.rb | 5 - app/models/application_setting.rb | 2 - .../admin/application_settings/_form.html.haml | 16 ---- ..._blocking_settings_from_application_settings.rb | 6 ++ db/schema.rb | 4 +- lib/dnsxl_check.rb | 105 --------------------- lib/gitlab/ip_check.rb | 34 ------- spec/lib/dnsxl_check_spec.rb | 68 ------------- 10 files changed, 8 insertions(+), 235 deletions(-) create mode 100644 db/migrate/20160128212447_remove_ip_blocking_settings_from_application_settings.rb delete mode 100644 lib/dnsxl_check.rb delete mode 100644 lib/gitlab/ip_check.rb delete mode 100644 spec/lib/dnsxl_check_spec.rb diff --git a/CHANGELOG b/CHANGELOG index fe0504ec996..9dec6f9809e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,7 @@ v 8.5.0 (unreleased) - Track project import failure - Fix visibility level text in admin area (Zeger-Jan van de Weg) - Update the ExternalIssue regex pattern (Blake Hitchcock) + - Revert "Add IP check against DNSBLs at account sign-up" - Deprecate API "merge_request/:merge_request_id/comments". Use "merge_requests/:merge_request_id/notes" instead - Deprecate API "merge_request/:merge_request_id/...". Use "merge_requests/:merge_request_id/..." instead diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 094eef28a43..9943745208e 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -74,8 +74,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :metrics_timeout, :metrics_method_call_threshold, :metrics_sample_interval, - :ip_blocking_enabled, - :dnsbl_servers_list, :recaptcha_enabled, :recaptcha_site_key, :recaptcha_private_key, diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 5efdd613e79..c48175a4c5a 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -8,11 +8,6 @@ class RegistrationsController < Devise::RegistrationsController def create if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha - if Gitlab::IpCheck.new(request.remote_ip).spam? - flash[:alert] = 'Could not create an account. This IP is listed for spam.' - return render action: 'new' - end - super else flash[:alert] = "There was an error with the reCAPTCHA code below. Please re-enter the code." diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 2f3487b53ac..59563b8823c 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -43,8 +43,6 @@ # metrics_port :integer default(8089) # sentry_enabled :boolean default(FALSE) # sentry_dsn :string -# ip_blocking_enabled :boolean default(FALSE) -# dns_blacklist_threshold :float default(0.33) # class ApplicationSetting < ActiveRecord::Base diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index baadca09518..c4fb2accdd0 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -212,22 +212,6 @@ %fieldset %legend Spam and Anti-bot Protection - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :ip_blocking_enabled do - = f.check_box :ip_blocking_enabled - Enable IP check against blacklist at sign-up - .help-block Helps preventing accounts creation from 'known spam sources' - - .form-group - = f.label :dnsbl_servers_list, class: 'control-label col-sm-2' do - DNSBL servers list - .col-sm-10 - = f.text_field :dnsbl_servers_list, class: 'form-control' - .help-block - Please enter DNSBL servers separated with comma - .form-group .col-sm-offset-2.col-sm-10 .checkbox diff --git a/db/migrate/20160128212447_remove_ip_blocking_settings_from_application_settings.rb b/db/migrate/20160128212447_remove_ip_blocking_settings_from_application_settings.rb new file mode 100644 index 00000000000..41821cdcc42 --- /dev/null +++ b/db/migrate/20160128212447_remove_ip_blocking_settings_from_application_settings.rb @@ -0,0 +1,6 @@ +class RemoveIpBlockingSettingsFromApplicationSettings < ActiveRecord::Migration + def change + remove_column :application_settings, :ip_blocking_enabled, :boolean, default: false + remove_column :application_settings, :dnsbl_servers_list, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index 97594011a02..2a2911bfbc7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160120172143) do +ActiveRecord::Schema.define(version: 20160128212447) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -64,8 +64,6 @@ ActiveRecord::Schema.define(version: 20160120172143) do t.integer "metrics_sample_interval", default: 15 t.boolean "sentry_enabled", default: false t.string "sentry_dsn" - t.boolean "ip_blocking_enabled", default: false - t.text "dnsbl_servers_list" end create_table "audit_events", force: :cascade do |t| diff --git a/lib/dnsxl_check.rb b/lib/dnsxl_check.rb deleted file mode 100644 index 1e506b2d9cb..00000000000 --- a/lib/dnsxl_check.rb +++ /dev/null @@ -1,105 +0,0 @@ -require 'resolv' - -class DNSXLCheck - - class Resolver - def self.search(query) - begin - Resolv.getaddress(query) - true - rescue Resolv::ResolvError - false - end - end - end - - IP_REGEXP = /\A(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\z/ - DEFAULT_THRESHOLD = 0.33 - - def self.create_from_list(list) - dnsxl_check = DNSXLCheck.new - - list.each do |entry| - dnsxl_check.add_list(entry.domain, entry.weight) - end - - dnsxl_check - end - - def test(ip) - if use_threshold? - test_with_threshold(ip) - else - test_strict(ip) - end - end - - def test_with_threshold(ip) - return false if lists.empty? - - search(ip) - final_score >= threshold - end - - def test_strict(ip) - return false if lists.empty? - - search(ip) - @score > 0 - end - - def use_threshold=(value) - @use_threshold = value == true - end - - def use_threshold? - @use_threshold &&= true - end - - def threshold=(threshold) - raise ArgumentError, "'threshold' value must be grather than 0 and less than or equal to 1" unless threshold > 0 && threshold <= 1 - @threshold = threshold - end - - def threshold - @threshold ||= DEFAULT_THRESHOLD - end - - def add_list(domain, weight) - @lists ||= [] - @lists << { domain: domain, weight: weight } - end - - def lists - @lists ||= [] - end - - private - - def search(ip) - raise ArgumentError, "'ip' value must be in #{IP_REGEXP} format" unless ip.match(IP_REGEXP) - - @score = 0 - - reversed = reverse_ip(ip) - search_in_rbls(reversed) - end - - def reverse_ip(ip) - ip.split('.').reverse.join('.') - end - - def search_in_rbls(reversed_ip) - lists.each do |rbl| - query = "#{reversed_ip}.#{rbl[:domain]}" - @score += rbl[:weight] if Resolver.search(query) - end - end - - def final_score - weights = lists.map{ |rbl| rbl[:weight] }.reduce(:+).to_i - return 0 if weights == 0 - - (@score.to_f / weights.to_f).round(2) - end -end diff --git a/lib/gitlab/ip_check.rb b/lib/gitlab/ip_check.rb deleted file mode 100644 index f2e9b50d225..00000000000 --- a/lib/gitlab/ip_check.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Gitlab - class IpCheck - - def initialize(ip) - @ip = ip - - application_settings = ApplicationSetting.current - @ip_blocking_enabled = application_settings.ip_blocking_enabled - @dnsbl_servers_list = application_settings.dnsbl_servers_list - end - - def spam? - @ip_blocking_enabled && blacklisted? - end - - private - - def blacklisted? - on_dns_blacklist? - end - - def on_dns_blacklist? - dnsbl_check = DNSXLCheck.new - prepare_dnsbl_list(dnsbl_check) - dnsbl_check.test(@ip) - end - - def prepare_dnsbl_list(dnsbl_check) - @dnsbl_servers_list.split(',').map(&:strip).reject(&:empty?).each do |domain| - dnsbl_check.add_list(domain, 1) - end - end - end -end diff --git a/spec/lib/dnsxl_check_spec.rb b/spec/lib/dnsxl_check_spec.rb deleted file mode 100644 index a35a1be0c90..00000000000 --- a/spec/lib/dnsxl_check_spec.rb +++ /dev/null @@ -1,68 +0,0 @@ -require 'spec_helper' -require 'ostruct' - -describe 'DNSXLCheck', lib: true, no_db: true do - let(:spam_ip) { '127.0.0.2' } - let(:no_spam_ip) { '127.0.0.3' } - let(:invalid_ip) { 'a.b.c.d' } - let!(:dnsxl_check) { DNSXLCheck.create_from_list([OpenStruct.new({ domain: 'test', weight: 1 })]) } - - before(:context) do - class DNSXLCheck::Resolver - class << self - alias_method :old_search, :search - def search(query) - return false if query.match(/always\.failing\.domain\z/) - return true if query.match(/\A2\.0\.0\.127\./) - return false if query.match(/\A3\.0\.0\.127\./) - end - end - end - end - - describe '#test' do - before do - dnsxl_check.threshold = 0.75 - dnsxl_check.add_list('always.failing.domain', 1) - end - - context 'when threshold is used' do - before { dnsxl_check.use_threshold= true } - - it { expect(dnsxl_check.test(spam_ip)).to be_falsey } - end - - context 'when threshold is not used' do - before { dnsxl_check.use_threshold= false } - - it { expect(dnsxl_check.test(spam_ip)).to be_truthy } - end - end - - describe '#test_with_threshold' do - it { expect{ dnsxl_check.test_with_threshold(invalid_ip) }.to raise_error(ArgumentError) } - - it { expect(dnsxl_check.test_with_threshold(spam_ip)).to be_truthy } - it { expect(dnsxl_check.test_with_threshold(no_spam_ip)).to be_falsey } - end - - describe '#test_strict' do - before do - dnsxl_check.threshold = 1 - dnsxl_check.add_list('always.failing.domain', 1) - end - - it { expect{ dnsxl_check.test_strict(invalid_ip) }.to raise_error(ArgumentError) } - - it { expect(dnsxl_check.test_with_threshold(spam_ip)).to be_falsey } - it { expect(dnsxl_check.test_with_threshold(no_spam_ip)).to be_falsey } - it { expect(dnsxl_check.test_strict(spam_ip)).to be_truthy } - it { expect(dnsxl_check.test_strict(no_spam_ip)).to be_falsey } - end - - describe '#threshold=' do - it { expect{ dnsxl_check.threshold = 0 }.to raise_error(ArgumentError) } - it { expect{ dnsxl_check.threshold = 1.1 }.to raise_error(ArgumentError) } - it { expect{ dnsxl_check.threshold = 0.5 }.not_to raise_error } - end -end -- cgit v1.2.1 From 92c04ecbd7308b7ccb53bb4a8adac8706285f481 Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Thu, 28 Jan 2016 17:06:21 -0800 Subject: Make changes in config/initializers/1_settings.rb --- doc/administration/environment_variables.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md index 42a27dcf6d6..0faef526d43 100644 --- a/doc/administration/environment_variables.md +++ b/doc/administration/environment_variables.md @@ -47,6 +47,7 @@ GITLAB_DATABASE_PORT | 5432 ## Adding more variables We welcome merge requests to make more settings configurable via variables. +Please make changes in the file config/initializers/1_settings.rb Please stick to the naming scheme "GITLAB_#{name 1_settings.rb in upper case}". ## Omnibus configuration -- cgit v1.2.1 From bbe0fa91d01fb9bc70d54e07ee393e857d92a267 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 28 Jan 2016 20:45:03 -0500 Subject: Prevent transient Capybara timeouts during feature tests The problem occurred because asset compilation takes a long time, so when the asset cache didn't exist and the first test ran, it would often (randomly) time out during the generation before the actual test even had a chance to run. Now we check if the cache exists before the suite runs, and if not, we manually fire a request to the root URL in order to generate it. This should allow subsequent tests to use the cached assets. --- features/support/capybara.rb | 8 ++++---- spec/support/capybara.rb | 6 ++++++ spec/support/test_env.rb | 17 ++++++++++++++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/features/support/capybara.rb b/features/support/capybara.rb index 4156c7ec484..38069ff8835 100644 --- a/features/support/capybara.rb +++ b/features/support/capybara.rb @@ -9,10 +9,6 @@ Capybara.register_driver :poltergeist do |app| Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout) end -Spinach.hooks.on_tag("javascript") do - Capybara.current_driver = Capybara.javascript_driver -end - Capybara.default_wait_time = timeout Capybara.ignore_hidden_elements = false @@ -22,3 +18,7 @@ unless ENV['CI'] || ENV['CI_SERVER'] # Keep only the screenshots generated from the last failing test suite Capybara::Screenshot.prune_strategy = :keep_last_run end + +Spinach.hooks.before_run do + TestEnv.warm_asset_cache +end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index fed1ab6ee33..a698f484df1 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -19,3 +19,9 @@ unless ENV['CI'] || ENV['CI_SERVER'] # Keep only the screenshots generated from the last failing test suite Capybara::Screenshot.prune_strategy = :keep_last_run end + +RSpec.configure do |config| + config.before(:suite) do + TestEnv.warm_asset_cache + end +end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 4f4743bff6d..0d1bd030f3c 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -146,6 +146,22 @@ module TestEnv FileUtils.chmod_R 0755, target_repo_path end + # When no cached assets exist, manually hit the root path to create them + # + # Otherwise they'd be created by the first test, often timing out and + # causing a transient test failure + def warm_asset_cache + return if warm_asset_cache? + return unless defined?(Capybara) + + Capybara.current_session.driver.visit '/' + end + + def warm_asset_cache? + cache = Rails.root.join(*%w(tmp cache assets test)) + Dir.exist?(cache) && Dir.entries(cache).length > 2 + end + private def factory_repo_path @@ -172,7 +188,6 @@ module TestEnv 'gitlab-test-fork' end - # Prevent developer git configurations from being persisted to test # repositories def git_env -- cgit v1.2.1 From 87d3ce88c703c0dd1a7fb99a5371466b19a461c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 29 Jan 2016 10:39:19 +0100 Subject: Improve GitLab flow documentation regarding the WIP merge requests [ci skip] --- doc/workflow/gitlab_flow.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index 8965e5b3654..be32f0c720b 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -152,9 +152,10 @@ The name of this branch should start with the issue number, for example '15-requ When you are done or want to discuss the code you open a merge request. This is an online place to discuss the change and review the code. -Creating a branch is a manual action since you do not always want to merge a new branch you push, it could be a long-running environment or release branch. -If you create the merge request but do not assign it to anyone it is a 'work-in-process' merge request. +Opening a merge request is a manual action since you do not always want to merge a new branch you push, it could be a long-running environment or release branch. +If you open the merge request but do not assign it to anyone it is a 'Work In Progress' merge request. These are used to discuss the proposed implementation but are not ready for inclusion in the master branch yet. +_Pro tip:_ Start the title of the merge request with `[WIP]` or `WIP:` to prevent it from being merged before it's ready. When the author thinks the code is ready the merge request is assigned to reviewer. The reviewer presses the merge button when they think the code is ready for inclusion in the master branch. @@ -185,7 +186,7 @@ If you have an issue that spans across multiple repositories, the best thing is ![Vim screen showing the rebase view](rebase.png) -With git you can use an interactive rebase (rebase -i) to squash multiple commits into one and reorder them. +With git you can use an interactive rebase (`rebase -i`) to squash multiple commits into one and reorder them. This functionality is useful if you made a couple of commits for small changes during development and want to replace them with a single commit or if you want to make the order more logical. However you should never rebase commits you have pushed to a remote server. Somebody can have referred to the commits or cherry-picked them. -- cgit v1.2.1 From 1b978727933e50ec4ddcd855f7f8a6216885ac4a Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 29 Jan 2016 12:15:58 +0000 Subject: Auto-focus emoji award search field Closes #12621 --- app/assets/javascripts/awards_handler.coffee | 1 + app/views/votes/_votes_block.html.haml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 1ef31c7700e..047df4786a9 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -4,6 +4,7 @@ class @AwardsHandler event.stopPropagation() event.preventDefault() $(".emoji-menu").show() + $("#emoji_search").focus() $("html").on 'click', (event) -> if !$(event.target).closest(".emoji-menu").length diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml index b1f8645eea0..91c5b7eac5e 100644 --- a/app/views/votes/_votes_block.html.haml +++ b/app/views/votes/_votes_block.html.haml @@ -7,7 +7,7 @@ - if current_user .awards-controls - %a.add-award{"data-toggle" => "dropdown", "data-target" => "#", "href" => "#"} + %a.add-award{"href" => "#"} = icon('smile-o') .emoji-menu .emoji-menu-content -- cgit v1.2.1 From 5a04fc90996b233a294d4f679a0cc23877f187f1 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 29 Jan 2016 12:58:34 +0000 Subject: Added tests to emoji search field focus --- features/project/issues/award_emoji.feature | 3 +++ features/steps/project/issues/award_emoji.rb | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/features/project/issues/award_emoji.feature b/features/project/issues/award_emoji.feature index 9a06fdc2ee6..bfde89fd896 100644 --- a/features/project/issues/award_emoji.feature +++ b/features/project/issues/award_emoji.feature @@ -9,6 +9,7 @@ Feature: Award Emoji @javascript Scenario: I add and remove award in the issue Given I click to emoji-picker + Then The search field is focused And I click to emoji in the picker Then I have award added And I can remove it by clicking to icon @@ -16,11 +17,13 @@ Feature: Award Emoji @javascript Scenario: I can see the list of emoji categories Given I click to emoji-picker + Then The search field is focused Then I can see the activity and food categories @javascript Scenario: I can search emoji Given I click to emoji-picker + Then The search field is focused And I search "hand" Then I see search result for "hand" diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb index 2c2ed08655e..69695d493f3 100644 --- a/features/steps/project/issues/award_emoji.rb +++ b/features/steps/project/issues/award_emoji.rb @@ -66,4 +66,8 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps expect(page).to have_selector '[data-emoji="raised_hand"]' end end + + step 'The search field is focused' do + page.evaluate_script("document.activeElement.id").should eq "emoji_search" + end end -- cgit v1.2.1 From 91b9cbff8de538c2966bca94c517ee6aa346f79b Mon Sep 17 00:00:00 2001 From: Josh Frye Date: Fri, 22 Jan 2016 11:13:37 -0800 Subject: First pass at deleting projects in the background. --- CHANGELOG | 3 +++ app/controllers/projects_controller.rb | 8 ++++++-- app/models/project.rb | 1 + app/services/delete_user_service.rb | 2 +- app/services/destroy_group_service.rb | 2 +- app/services/projects/destroy_service.rb | 6 ++++++ app/workers/project_destroy_worker.rb | 17 +++++++++++++++++ .../20160122185421_add_pending_delete_to_project.rb | 5 +++++ db/schema.rb | 7 ++++--- lib/api/projects.rb | 4 ++-- spec/models/hooks/system_hook_spec.rb | 6 +++--- 11 files changed, 49 insertions(+), 12 deletions(-) create mode 100644 app/workers/project_destroy_worker.rb create mode 100644 db/migrate/20160122185421_add_pending_delete_to_project.rb diff --git a/CHANGELOG b/CHANGELOG index 9dec6f9809e..8a6a4042cb0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ Please view this file on the master branch, on stable branches it's out of date. +v 8.6.0 (unreleased) + - Delete project and associations in a background worker + v 8.5.0 (unreleased) - Ensure rake tasks that don't need a DB connection can be run without one - Add "visibility" flag to GET /projects api endpoint diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 935f7d75c6a..4df5095bd94 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -93,6 +93,10 @@ class ProjectsController < ApplicationController return end + if @project.pending_delete? + flash[:alert] = "Project queued for delete." + end + respond_to do |format| format.html do if @project.repository_exists? @@ -120,8 +124,8 @@ class ProjectsController < ApplicationController def destroy return access_denied! unless can?(current_user, :remove_project, @project) - ::Projects::DestroyService.new(@project, current_user, {}).execute - flash[:alert] = "Project '#{@project.name}' was deleted." + ::Projects::DestroyService.new(@project, current_user, {}).pending_delete! + flash[:alert] = "Project '#{@project.name}' will be deleted." redirect_to dashboard_projects_path rescue Projects::DestroyService::DestroyError => ex diff --git a/app/models/project.rb b/app/models/project.rb index 238932f59a7..043f08b9a13 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -36,6 +36,7 @@ # build_coverage_regex :string # build_allow_git_fetch :boolean default(TRUE), not null # build_timeout :integer default(3600), not null +# pending_delete :boolean # require 'carrierwave/orm/activerecord' diff --git a/app/services/delete_user_service.rb b/app/services/delete_user_service.rb index e622fd5ea5d..173e50c9206 100644 --- a/app/services/delete_user_service.rb +++ b/app/services/delete_user_service.rb @@ -13,7 +13,7 @@ class DeleteUserService user.personal_projects.each do |project| # Skip repository removal because we remove directory with namespace # that contain all this repositories - ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute + ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete! end user.destroy diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb index d929a676293..9189de390a2 100644 --- a/app/services/destroy_group_service.rb +++ b/app/services/destroy_group_service.rb @@ -9,7 +9,7 @@ class DestroyGroupService @group.projects.each do |project| # Skip repository removal because we remove directory with namespace # that contain all this repositories - ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute + ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete! end @group.destroy diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 28872c89259..294157b4f0e 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -6,6 +6,12 @@ module Projects DELETED_FLAG = '+deleted' + def pending_delete! + project.update_attribute(:pending_delete, true) + + ProjectDestroyWorker.perform_in(1.minute, project.id, current_user.id, params) + end + def execute return false unless can?(current_user, :remove_project, project) diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb new file mode 100644 index 00000000000..d06e4480292 --- /dev/null +++ b/app/workers/project_destroy_worker.rb @@ -0,0 +1,17 @@ +class ProjectDestroyWorker + include Sidekiq::Worker + + sidekiq_options queue: :default + + def perform(project_id, user_id, params) + begin + project = Project.find(project_id) + rescue ActiveRecord::RecordNotFound + return + end + + user = User.find(user_id) + + ::Projects::DestroyService.new(project, user, params).execute + end +end diff --git a/db/migrate/20160122185421_add_pending_delete_to_project.rb b/db/migrate/20160122185421_add_pending_delete_to_project.rb new file mode 100644 index 00000000000..457922942b0 --- /dev/null +++ b/db/migrate/20160122185421_add_pending_delete_to_project.rb @@ -0,0 +1,5 @@ +class AddPendingDeleteToProject < ActiveRecord::Migration + def change + add_column :projects, :pending_delete, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index 2a2911bfbc7..8c9cbe73c87 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -677,6 +677,7 @@ ActiveRecord::Schema.define(version: 20160128212447) do t.string "build_coverage_regex" t.boolean "build_allow_git_fetch", default: true, null: false t.integer "build_timeout", default: 3600, null: false + t.boolean "pending_delete" end add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree @@ -728,9 +729,9 @@ ActiveRecord::Schema.define(version: 20160128212447) do t.string "type" t.string "title" t.integer "project_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "active", null: false + t.datetime "created_at" + t.datetime "updated_at" + t.boolean "active", default: false, null: false t.text "properties" t.boolean "template", default: false t.boolean "push_events", default: true diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 71bb342f844..1f991e600e3 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -187,7 +187,7 @@ module API else present @forked_project, with: Entities::Project, user_can_admin_project: can?(current_user, :admin_project, @forked_project) - end + end end # Update an existing project @@ -246,7 +246,7 @@ module API # DELETE /projects/:id delete ":id" do authorize! :remove_project, user_project - ::Projects::DestroyService.new(user_project, current_user, {}).execute + ::Projects::DestroyService.new(user_project, current_user, {}).pending_delete! end # Mark this project as forked from another diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb index 138b87a9a06..fd1513cab1b 100644 --- a/spec/models/hooks/system_hook_spec.rb +++ b/spec/models/hooks/system_hook_spec.rb @@ -36,7 +36,7 @@ describe SystemHook, models: true do it "project_destroy hook" do user = create(:user) project = create(:empty_project, namespace: user.namespace) - Projects::DestroyService.new(project, user, {}).execute + Projects::DestroyService.new(project, user, {}).pending_delete! expect(WebMock).to have_requested(:post, @system_hook.url).with( body: /project_destroy/, headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } @@ -65,7 +65,7 @@ describe SystemHook, models: true do project = create(:project) project.team << [user, :master] expect(WebMock).to have_requested(:post, @system_hook.url).with( - body: /user_add_to_team/, + body: /user_add_to_team/, headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } ).once end @@ -76,7 +76,7 @@ describe SystemHook, models: true do project.team << [user, :master] project.project_members.destroy_all expect(WebMock).to have_requested(:post, @system_hook.url).with( - body: /user_remove_from_team/, + body: /user_remove_from_team/, headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } ).once end -- cgit v1.2.1 From f255313ea47b433b428b3b90452aede8c9a6618c Mon Sep 17 00:00:00 2001 From: Josh Frye Date: Thu, 28 Jan 2016 08:48:46 -0500 Subject: Update CHANGELOG --- CHANGELOG | 4 +--- db/migrate/20160122185421_add_pending_delete_to_project.rb | 2 +- db/schema.rb | 8 ++++---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8a6a4042cb0..66ca75e70da 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,8 +1,5 @@ Please view this file on the master branch, on stable branches it's out of date. -v 8.6.0 (unreleased) - - Delete project and associations in a background worker - v 8.5.0 (unreleased) - Ensure rake tasks that don't need a DB connection can be run without one - Add "visibility" flag to GET /projects api endpoint @@ -41,6 +38,7 @@ v 8.4.1 and Nokogiri (1.6.7.2) - Fix redirect loop during import - Fix diff highlighting for all syntax themes + - Delete project and associations in a background worker v 8.4.0 - Allow LDAP users to change their email if it was not set by the LDAP server diff --git a/db/migrate/20160122185421_add_pending_delete_to_project.rb b/db/migrate/20160122185421_add_pending_delete_to_project.rb index 457922942b0..046a5d8fc32 100644 --- a/db/migrate/20160122185421_add_pending_delete_to_project.rb +++ b/db/migrate/20160122185421_add_pending_delete_to_project.rb @@ -1,5 +1,5 @@ class AddPendingDeleteToProject < ActiveRecord::Migration def change - add_column :projects, :pending_delete, :boolean + add_column :projects, :pending_delete, :boolean, default: false end end diff --git a/db/schema.rb b/db/schema.rb index 8c9cbe73c87..73589cb1f17 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -677,7 +677,7 @@ ActiveRecord::Schema.define(version: 20160128212447) do t.string "build_coverage_regex" t.boolean "build_allow_git_fetch", default: true, null: false t.integer "build_timeout", default: 3600, null: false - t.boolean "pending_delete" + t.boolean "pending_delete", default: false end add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree @@ -729,9 +729,9 @@ ActiveRecord::Schema.define(version: 20160128212447) do t.string "type" t.string "title" t.integer "project_id" - t.datetime "created_at" - t.datetime "updated_at" - t.boolean "active", default: false, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "active", null: false t.text "properties" t.boolean "template", default: false t.boolean "push_events", default: true -- cgit v1.2.1 From 873f336ecc64f958079589e2144860da9d79a76b Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Fri, 29 Jan 2016 08:28:58 -0600 Subject: Increase LFS objects size column --- CHANGELOG | 3 +++ db/migrate/20160128233227_change_lfs_objects_size_column.rb | 5 +++++ db/schema.rb | 6 +++--- 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20160128233227_change_lfs_objects_size_column.rb diff --git a/CHANGELOG b/CHANGELOG index 9dec6f9809e..e5a32d3b49e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -19,6 +19,9 @@ v 8.5.0 (unreleased) - Deprecate API "merge_request/:merge_request_id/comments". Use "merge_requests/:merge_request_id/notes" instead - Deprecate API "merge_request/:merge_request_id/...". Use "merge_requests/:merge_request_id/..." instead +v 8.4.3 + - Increase lfs_objects size column to 8-byte integer to allow files larger than 2.1GB + v 8.4.2 - Bump required gitlab-workhorse version to bring in a fix for missing artifacts in the build artifacts browser diff --git a/db/migrate/20160128233227_change_lfs_objects_size_column.rb b/db/migrate/20160128233227_change_lfs_objects_size_column.rb new file mode 100644 index 00000000000..e7fd1f71777 --- /dev/null +++ b/db/migrate/20160128233227_change_lfs_objects_size_column.rb @@ -0,0 +1,5 @@ +class ChangeLfsObjectsSizeColumn < ActiveRecord::Migration + def change + change_column :lfs_objects, :size, :integer, limit: 8 + end +end diff --git a/db/schema.rb b/db/schema.rb index 2a2911bfbc7..96b5c3af1e5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160128212447) do +ActiveRecord::Schema.define(version: 20160128233227) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -445,8 +445,8 @@ ActiveRecord::Schema.define(version: 20160128212447) do add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree create_table "lfs_objects", force: :cascade do |t| - t.string "oid", null: false - t.integer "size", null: false + t.string "oid", null: false + t.integer "size", limit: 8, null: false t.datetime "created_at" t.datetime "updated_at" t.string "file" -- cgit v1.2.1 From aa30088f6e02c761722d202bb741b7b51e9ec36e Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Wed, 27 Jan 2016 11:42:31 -0200 Subject: Fix old version warning when viewing wiki latest version via version_id --- app/models/wiki_page.rb | 2 +- spec/models/wiki_page_spec.rb | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 2a65f0431c4..dbd70dc5a44 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -110,7 +110,7 @@ class WikiPage # Returns boolean True or False if this instance # is an old version of the page. def historical? - @page.historical? + @page.historical? && versions.first.sha != version.sha end # Returns boolean True or False if this instance diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index c1b03838aa9..ddc49495eda 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -189,6 +189,38 @@ describe WikiPage, models: true do end end + describe '#historical?' do + before do + create_page('Update', 'content') + @page = wiki.find_page('Update') + 3.times { |i| @page.update("content #{i}") } + end + + after do + destroy_page('Update') + end + + it 'returns true when requesting an old version' do + old_version = @page.versions.last.to_s + old_page = wiki.find_page('Update', old_version) + + expect(old_page.historical?).to eq true + end + + it 'returns false when requesting latest version' do + latest_version = @page.versions.first.to_s + latest_page = wiki.find_page('Update', latest_version) + + expect(latest_page.historical?).to eq false + end + + it 'returns false when version is nil' do + latest_page = wiki.find_page('Update', nil) + + expect(latest_page.historical?).to eq false + end + end + private def remove_temp_repo(path) -- cgit v1.2.1 From 677b4db9e682b29bb15dddb84fe80b976490a671 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 29 Jan 2016 19:37:17 +0100 Subject: Mark inline difference between old and new paths when a file is renamed --- CHANGELOG | 1 + app/assets/stylesheets/framework/files.scss | 26 +++++++++ app/helpers/diff_helper.rb | 11 +++- app/views/notify/repository_push_email.html.haml | 2 +- app/views/projects/diffs/_file.html.haml | 22 ++++--- lib/gitlab/diff/highlight.rb | 17 ++---- lib/gitlab/diff/inline_diff.rb | 57 +++++++++++------- lib/gitlab/diff/inline_diff_marker.rb | 14 +++-- spec/fixtures/parallel_diff_result.yml | 2 +- spec/lib/gitlab/diff/highlight_spec.rb | 74 ++++++++++++++++++------ spec/lib/gitlab/diff/inline_diff_marker_spec.rb | 26 +++++++-- spec/lib/gitlab/diff/inline_diff_spec.rb | 17 +++++- 12 files changed, 194 insertions(+), 75 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9dec6f9809e..d309ef318cf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,6 +18,7 @@ v 8.5.0 (unreleased) - Revert "Add IP check against DNSBLs at account sign-up" - Deprecate API "merge_request/:merge_request_id/comments". Use "merge_requests/:merge_request_id/notes" instead - Deprecate API "merge_request/:merge_request_id/...". Use "merge_requests/:merge_request_id/..." instead + - Mark inline difference between old and new paths when a file is renamed v 8.4.2 - Bump required gitlab-workhorse version to bring in a fix for missing diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 00cb756b376..c7f3604850d 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -36,6 +36,20 @@ } } + .filename { + &.old { + span.idiff { + background-color: #f8cbcb; + } + } + + &.new { + span.idiff { + background-color: #a6f3a6; + } + } + } + .left-options { margin-top: -3px; } @@ -158,3 +172,15 @@ } } } + +span.idiff { + &.left { + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; + } + + &.right { + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + } +} diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 62971d1e14b..0b4fda12788 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -1,4 +1,13 @@ module DiffHelper + def mark_inline_diffs(old_line, new_line) + old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_line, new_line).inline_diffs + + marked_old_line = Gitlab::Diff::InlineDiffMarker.new(old_line).mark(old_diffs).html_safe + marked_new_line = Gitlab::Diff::InlineDiffMarker.new(new_line).mark(new_diffs).html_safe + + [marked_old_line, marked_new_line] + end + def diff_view params[:view] == 'parallel' ? 'parallel' : 'inline' end @@ -55,7 +64,7 @@ module DiffHelper if line.blank? "  ".html_safe else - line.html_safe + line end end diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index 3dd2595f1ad..f2e405b14fd 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -18,7 +18,7 @@ %div %span by #{commit.author_name} %i at #{commit.committed_date.to_s(:iso8601)} - %pre.commit-message + %pre.commit-message = commit.safe_message %h4 #{pluralize @message.diffs_count, "changed file"}: diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index fc0eaef2286..3ac058a3bf8 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -7,16 +7,20 @@ = submodule_link(blob, @commit.id, project.repository) - else = blob_icon blob.mode, blob.name - = link_to "#diff-#{i}" do - %strong - = diff_file.new_path - - if diff_file.deleted_file - deleted - - elsif diff_file.renamed_file - renamed from - %strong - = diff_file.old_path + = link_to "#diff-#{i}" do + - if diff_file.renamed_file + - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) + %strong.filename.old + = old_path + → + %strong.filename.new + = new_path + - else + %strong + = diff_file.new_path + - if diff_file.deleted_file + deleted - if diff_file.mode_changed? %small diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index a7f925ce134..9429b3ff88d 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -21,13 +21,13 @@ module Gitlab # ignore highlighting for "match" lines next diff_line if diff_line.type == 'match' || diff_line.type == 'nonewline' - rich_line = highlight_line(diff_line, i) + rich_line = highlight_line(diff_line) || diff_line.text if line_inline_diffs = inline_diffs[i] rich_line = InlineDiffMarker.new(diff_line.text, rich_line).mark(line_inline_diffs) end - diff_line.text = rich_line.html_safe + diff_line.text = rich_line diff_line end @@ -35,8 +35,8 @@ module Gitlab private - def highlight_line(diff_line, index) - return html_escape(diff_line.text) unless diff_file && diff_file.diff_refs + def highlight_line(diff_line) + return unless diff_file && diff_file.diff_refs line_prefix = diff_line.text.match(/\A(.)/) ? $1 : ' ' @@ -49,11 +49,11 @@ module Gitlab # Only update text if line is found. This will prevent # issues with submodules given the line only exists in diff content. - rich_line ? line_prefix + rich_line : html_escape(diff_line.text) + "#{line_prefix}#{rich_line}".html_safe if rich_line end def inline_diffs - @inline_diffs ||= InlineDiff.new(@raw_lines).inline_diffs + @inline_diffs ||= InlineDiff.for_lines(@raw_lines) end def old_lines @@ -72,11 +72,6 @@ module Gitlab [ref.project.repository, ref.id, path] end - - def html_escape(str) - replacements = { '&' => '&', '>' => '>', '<' => '<', '"' => '"', "'" => ''' } - str.gsub(/[&"'><]/, replacements) - end end end end diff --git a/lib/gitlab/diff/inline_diff.rb b/lib/gitlab/diff/inline_diff.rb index b8a61ad6115..789c14518b0 100644 --- a/lib/gitlab/diff/inline_diff.rb +++ b/lib/gitlab/diff/inline_diff.rb @@ -1,43 +1,58 @@ module Gitlab module Diff class InlineDiff - attr_accessor :lines + attr_accessor :old_line, :new_line, :offset - def initialize(lines) - @lines = lines - end + def self.for_lines(lines) + local_edit_indexes = self.find_local_edits(lines) - def inline_diffs inline_diffs = [] local_edit_indexes.each do |index| old_index = index new_index = index + 1 - old_line = @lines[old_index] - new_line = @lines[new_index] - - # Skip inline diff if empty line was replaced with content - next if old_line[1..-1] == "" - - # Add one, because this is based on the prefixless version - lcp = longest_common_prefix(old_line[1..-1], new_line[1..-1]) + 1 - lcs = longest_common_suffix(old_line[lcp..-1], new_line[lcp..-1]) + old_line = lines[old_index] + new_line = lines[new_index] - old_diff_range = lcp..(old_line.length - lcs - 1) - new_diff_range = lcp..(new_line.length - lcs - 1) + old_diffs, new_diffs = new(old_line, new_line, offset: 1).inline_diffs - inline_diffs[old_index] = [old_diff_range] if old_diff_range.begin <= old_diff_range.end - inline_diffs[new_index] = [new_diff_range] if new_diff_range.begin <= new_diff_range.end + inline_diffs[old_index] = old_diffs + inline_diffs[new_index] = new_diffs end inline_diffs end + def initialize(old_line, new_line, offset: 0) + @old_line = old_line[offset..-1] + @new_line = new_line[offset..-1] + @offset = offset + end + + def inline_diffs + # Skip inline diff if empty line was replaced with content + return if old_line == "" + + lcp = longest_common_prefix(old_line, new_line) + lcs = longest_common_suffix(old_line[lcp..-1], new_line[lcp..-1]) + + lcp += offset + old_length = old_line.length + offset + new_length = new_line.length + offset + + old_diff_range = lcp..(old_length - lcs - 1) + new_diff_range = lcp..(new_length - lcs - 1) + + old_diffs = [old_diff_range] if old_diff_range.begin <= old_diff_range.end + new_diffs = [new_diff_range] if new_diff_range.begin <= new_diff_range.end + + [old_diffs, new_diffs] + end + private - # Find runs of single line edits - def local_edit_indexes - line_prefixes = @lines.map { |line| line.match(/\A([+-])/) ? $1 : ' ' } + def self.find_local_edits(lines) + line_prefixes = lines.map { |line| line.match(/\A([+-])/) ? $1 : ' ' } joined_line_prefixes = " #{line_prefixes.join} " offset = 0 diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb index 1d7fa1bce06..dccb717e95d 100644 --- a/lib/gitlab/diff/inline_diff_marker.rb +++ b/lib/gitlab/diff/inline_diff_marker.rb @@ -5,10 +5,12 @@ module Gitlab def initialize(raw_line, rich_line = raw_line) @raw_line = raw_line - @rich_line = rich_line + @rich_line = ERB::Util.html_escape(rich_line) end def mark(line_inline_diffs) + return rich_line unless line_inline_diffs + marker_ranges = [] line_inline_diffs.each do |inline_diff_range| # Map the inline-diff range based on the raw line to character positions in the rich line @@ -19,11 +21,15 @@ module Gitlab offset = 0 # Mark each range - marker_ranges.each do |range| - offset = insert_around_range(rich_line, range, "", "", offset) + marker_ranges.each_with_index do |range, i| + class_names = ["idiff"] + class_names << "left" if i == 0 + class_names << "right" if i == marker_ranges.length - 1 + + offset = insert_around_range(rich_line, range, "", "", offset) end - rich_line + rich_line.html_safe end private diff --git a/spec/fixtures/parallel_diff_result.yml b/spec/fixtures/parallel_diff_result.yml index a326b651aad..a8b7907d4ba 100644 --- a/spec/fixtures/parallel_diff_result.yml +++ b/spec/fixtures/parallel_diff_result.yml @@ -55,7 +55,7 @@ :type: new :number: 9 :text: | - + raise RuntimeError, "System commands must be given as an array of strings" + + raise RuntimeError, "System commands must be given as an array of strings" :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9 - :left: :type: diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index b84a57f357a..deab16714e0 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -9,33 +9,69 @@ describe Gitlab::Diff::Highlight, lib: true do let(:diff_file) { Gitlab::Diff::File.new(diff, [commit.parent, commit]) } describe '#highlight' do - let(:diff_lines) { Gitlab::Diff::Highlight.new(diff_file).highlight } + context "with a diff file" do + let(:subject) { Gitlab::Diff::Highlight.new(diff_file).highlight } - it 'should return Gitlab::Diff::Line elements' do - expect(diff_lines.first).to be_an_instance_of(Gitlab::Diff::Line) - end + it 'should return Gitlab::Diff::Line elements' do + expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line) + end - it 'should not modify "match" lines' do - expect(diff_lines[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') - expect(diff_lines[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') - end + it 'should not modify "match" lines' do + expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') + expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') + end - it 'should highlight unchanged lines' do - code = %Q{ def popen(cmd, path=nil)\n} + it 'highlights and marks unchanged lines' do + code = %Q{ def popen(cmd, path=nil)\n} - expect(diff_lines[2].text).to eq(code) - end + expect(subject[2].text).to eq(code) + end - it 'should highlight removed lines' do - code = %Q{- raise "System commands must be given as an array of strings"\n} + it 'highlights and marks removed lines' do + code = %Q{- raise "System commands must be given as an array of strings"\n} - expect(diff_lines[4].text).to eq(code) - end + expect(subject[4].text).to eq(code) + end - it 'should highlight added lines' do - code = %Q{+ raise RuntimeError, "System commands must be given as an array of strings"\n} + it 'highlights and marks added lines' do + code = %Q{+ raise RuntimeError, "System commands must be given as an array of strings"\n} - expect(diff_lines[5].text).to eq(code) + expect(subject[5].text).to eq(code) + end end + + context "with diff lines" do + let(:subject) { Gitlab::Diff::Highlight.new(diff_file.diff_lines).highlight } + + it 'should return Gitlab::Diff::Line elements' do + expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line) + end + + it 'should not modify "match" lines' do + expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') + expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') + end + + it 'marks unchanged lines' do + code = %Q{ def popen(cmd, path=nil)} + + expect(subject[2].text).to eq(code) + expect(subject[2].text).not_to be_html_safe + end + + it 'marks removed lines' do + code = %Q{- raise "System commands must be given as an array of strings"} + + expect(subject[4].text).to eq(code) + expect(subject[4].text).not_to be_html_safe + end + + it 'marks added lines' do + code = %Q{+ raise RuntimeError, "System commands must be given as an array of strings"} + + expect(subject[5].text).to eq(code) + expect(subject[5].text).to be_html_safe + end + end end end diff --git a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb index 6f3276a8b53..ea5c31011f0 100644 --- a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb +++ b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb @@ -2,14 +2,28 @@ require 'spec_helper' describe Gitlab::Diff::InlineDiffMarker, lib: true do describe '#inline_diffs' do - let(:raw) { "abc 'def'" } - let(:rich) { %{abc 'def'} } - let(:inline_diffs) { [2..5] } - let(:subject) { Gitlab::Diff::InlineDiffMarker.new(raw, rich).mark(inline_diffs) } + context "when the rich text is html safe" do + let(:raw) { "abc 'def'" } + let(:rich) { %{abc 'def'}.html_safe } + let(:inline_diffs) { [2..5] } + let(:subject) { Gitlab::Diff::InlineDiffMarker.new(raw, rich).mark(inline_diffs) } - it 'marks the inline diffs' do - expect(subject).to eq(%{abc 'def'}) + it 'marks the inline diffs' do + expect(subject).to eq(%{abc 'def'}) + expect(subject).to be_html_safe + end + end + + context "when the text text is not html safe" do + let(:raw) { "abc 'def'" } + let(:inline_diffs) { [2..5] } + let(:subject) { Gitlab::Diff::InlineDiffMarker.new(raw).mark(inline_diffs) } + + it 'marks the inline diffs' do + expect(subject).to eq(%{abc 'def'}) + expect(subject).to be_html_safe + end end end end diff --git a/spec/lib/gitlab/diff/inline_diff_spec.rb b/spec/lib/gitlab/diff/inline_diff_spec.rb index 056917df893..95a993d26cf 100644 --- a/spec/lib/gitlab/diff/inline_diff_spec.rb +++ b/spec/lib/gitlab/diff/inline_diff_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Diff::InlineDiff, lib: true do - describe '#inline_diffs' do + describe '.for_lines' do let(:diff) do < Date: Wed, 27 Jan 2016 12:20:17 -0500 Subject: Cache BroadcastMessage.current for 5 minutes. Fixes #12770 --- app/models/broadcast_message.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb index 61119633717..ab3778b6397 100644 --- a/app/models/broadcast_message.rb +++ b/app/models/broadcast_message.rb @@ -26,7 +26,9 @@ class BroadcastMessage < ActiveRecord::Base default_value_for :font, '#FFFFFF' def self.current - where("ends_at > :now AND starts_at <= :now", now: Time.zone.now).last + Rails.cache.fetch("broadcast_message_current", expires_in: 5.minutes) do + where("ends_at > :now AND starts_at <= :now", now: Time.zone.now).last + end end def active? -- cgit v1.2.1 From a6d6cfaf924f8002e807fda9bad03e866db2a4ec Mon Sep 17 00:00:00 2001 From: Josh Frye Date: Fri, 29 Jan 2016 13:38:57 -0500 Subject: Change ttl to 1 minute --- app/models/broadcast_message.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb index ab3778b6397..8a0a8a4c2a9 100644 --- a/app/models/broadcast_message.rb +++ b/app/models/broadcast_message.rb @@ -26,7 +26,7 @@ class BroadcastMessage < ActiveRecord::Base default_value_for :font, '#FFFFFF' def self.current - Rails.cache.fetch("broadcast_message_current", expires_in: 5.minutes) do + Rails.cache.fetch("broadcast_message_current", expires_in: 1.minute) do where("ends_at > :now AND starts_at <= :now", now: Time.zone.now).last end end -- cgit v1.2.1 From d97742570fce0512eddb6dc6c924d541aa4f57e4 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 29 Jan 2016 19:49:07 -0500 Subject: Increase the minimum length for commit SHA matching to 7 This is the git default and will help to prevent false positive matches. Closes #12706 --- app/models/commit.rb | 6 +++--- app/models/commit_range.rb | 4 ++-- config/routes.rb | 2 +- spec/lib/banzai/filter/commit_reference_filter_spec.rb | 4 ++-- spec/routing/project_routing_spec.rb | 8 ++++---- spec/support/filter_spec_helper.rb | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index 0ba7b584d91..23b771aebb7 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -68,18 +68,18 @@ class Commit # Pattern used to extract commit references from text # - # The SHA can be between 6 and 40 hex characters. + # The SHA can be between 7 and 40 hex characters. # # This pattern supports cross-project references. def self.reference_pattern %r{ (?:#{Project.reference_pattern}#{reference_prefix})? - (?\h{6,40}) + (?\h{7,40}) }x end def self.link_reference_pattern - super("commit", /(?\h{6,40})/) + super("commit", /(?\h{7,40})/) end def to_reference(from_project = nil) diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 14e7971fa06..289dbc57287 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -32,8 +32,8 @@ class CommitRange PATTERN = /#{REF_PATTERN}\.{2,3}#{REF_PATTERN}/ # In text references, the beginning and ending refs can only be SHAs - # between 6 and 40 hex characters. - STRICT_PATTERN = /\h{6,40}\.{2,3}\h{6,40}/ + # between 7 and 40 hex characters. + STRICT_PATTERN = /\h{7,40}\.{2,3}\h{7,40}/ def self.reference_prefix '@' diff --git a/config/routes.rb b/config/routes.rb index fdfdb449085..54cc338b605 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -490,7 +490,7 @@ Rails.application.routes.draw do end resource :avatar, only: [:show, :destroy] - resources :commit, only: [:show], constraints: { id: /[[:alnum:]]{6,40}/ } do + resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do member do get :branches get :builds diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb index 473534ba68a..63a32d9d455 100644 --- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb @@ -21,7 +21,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do let(:reference) { commit.id } # Let's test a variety of commit SHA sizes just to be paranoid - [6, 8, 12, 18, 20, 32, 40].each do |size| + [7, 8, 12, 18, 20, 32, 40].each do |size| it "links to a valid reference of #{size} characters" do doc = reference_filter("See #{reference[0...size]}") @@ -35,7 +35,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do doc = reference_filter("See #{commit.id}") expect(doc.text).to eq "See #{commit.short_id}" - doc = reference_filter("See #{commit.id[0...6]}") + doc = reference_filter("See #{commit.id[0...7]}") expect(doc.text).to eq "See #{commit.short_id}" end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 22937226fce..538f44e4f3f 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -321,12 +321,12 @@ describe Projects::HooksController, 'routing' do end end -# project_commit GET /:project_id/commit/:id(.:format) commit#show {id: /[[:alnum:]]{6,40}/, project_id: /[^\/]+/} +# project_commit GET /:project_id/commit/:id(.:format) commit#show {id: /\h{7,40}/, project_id: /[^\/]+/} describe Projects::CommitController, 'routing' do it 'to #show' do - expect(get('/gitlab/gitlabhq/commit/4246fb')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fb') - expect(get('/gitlab/gitlabhq/commit/4246fb.diff')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fb', format: 'diff') - expect(get('/gitlab/gitlabhq/commit/4246fb.patch')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fb', format: 'patch') + expect(get('/gitlab/gitlabhq/commit/4246fbd')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd') + expect(get('/gitlab/gitlabhq/commit/4246fbd.diff')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd', format: 'diff') + expect(get('/gitlab/gitlabhq/commit/4246fbd.patch')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd', format: 'patch') expect(get('/gitlab/gitlabhq/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5') end end diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb index d6e03cbef3d..ef5ea7d626e 100644 --- a/spec/support/filter_spec_helper.rb +++ b/spec/support/filter_spec_helper.rb @@ -67,9 +67,9 @@ module FilterSpecHelper if reference =~ /\A(.+)?.\d+\z/ # Integer-based reference with optional project prefix reference.gsub(/\d+\z/) { |i| i.to_i + 1 } - elsif reference =~ /\A(.+@)?(\h{6,40}\z)/ + elsif reference =~ /\A(.+@)?(\h{7,40}\z)/ # SHA-based reference with optional prefix - reference.gsub(/\h{6,40}\z/) { |v| v.reverse } + reference.gsub(/\h{7,40}\z/) { |v| v.reverse } else reference.gsub(/\w+\z/) { |v| v.reverse } end -- cgit v1.2.1 From fa0cbb13990a7e7edec55e9724e9c4d676743f3b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 30 Jan 2016 12:53:12 +0100 Subject: Fix specs and add a new one --- spec/helpers/diff_helper_spec.rb | 17 +++++++++--- spec/lib/gitlab/diff/highlight_spec.rb | 48 +++++++++++++++++----------------- 2 files changed, 37 insertions(+), 28 deletions(-) diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 955d2852cfd..14986a74c2e 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -104,8 +104,7 @@ describe DiffHelper do end end - describe 'diff_line_content' do - + describe '#diff_line_content' do it 'should return non breaking space when line is empty' do expect(diff_line_content(nil)).to eq('  ') end @@ -116,9 +115,19 @@ describe DiffHelper do expect(diff_line_content(diff_file.diff_lines.first.type)).to eq('match') expect(diff_file.diff_lines.first.new_pos).to eq(6) end + end + + describe "#mark_inline_diffs" do + let(:old_line) { %{abc 'def'} } + let(:new_line) { %{abc "def"} } + + it "returns strings with marked inline diffs" do + marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line) - it 'should return safe HTML' do - expect(diff_line_content(diff_file.diff_lines.first.text)).to be_html_safe + expect(marked_old_line).to eq("abc 'def'") + expect(marked_old_line).to be_html_safe + expect(marked_new_line).to eq("abc "def"") + expect(marked_new_line).to be_html_safe end end end diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index deab16714e0..d19bf4ac84b 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -40,38 +40,38 @@ describe Gitlab::Diff::Highlight, lib: true do end end - context "with diff lines" do - let(:subject) { Gitlab::Diff::Highlight.new(diff_file.diff_lines).highlight } + context "with diff lines" do + let(:subject) { Gitlab::Diff::Highlight.new(diff_file.diff_lines).highlight } - it 'should return Gitlab::Diff::Line elements' do - expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line) - end + it 'should return Gitlab::Diff::Line elements' do + expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line) + end - it 'should not modify "match" lines' do - expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') - expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') - end + it 'should not modify "match" lines' do + expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') + expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') + end - it 'marks unchanged lines' do - code = %Q{ def popen(cmd, path=nil)} + it 'marks unchanged lines' do + code = %Q{ def popen(cmd, path=nil)} - expect(subject[2].text).to eq(code) - expect(subject[2].text).not_to be_html_safe - end + expect(subject[2].text).to eq(code) + expect(subject[2].text).not_to be_html_safe + end - it 'marks removed lines' do - code = %Q{- raise "System commands must be given as an array of strings"} + it 'marks removed lines' do + code = %Q{- raise "System commands must be given as an array of strings"} - expect(subject[4].text).to eq(code) - expect(subject[4].text).not_to be_html_safe - end + expect(subject[4].text).to eq(code) + expect(subject[4].text).not_to be_html_safe + end - it 'marks added lines' do - code = %Q{+ raise RuntimeError, "System commands must be given as an array of strings"} + it 'marks added lines' do + code = %Q{+ raise RuntimeError, "System commands must be given as an array of strings"} - expect(subject[5].text).to eq(code) - expect(subject[5].text).to be_html_safe - end + expect(subject[5].text).to eq(code) + expect(subject[5].text).to be_html_safe end + end end end -- cgit v1.2.1 From 47afe7ae8ff33c756c9ff6e2339a82f07bf11c07 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 30 Jan 2016 12:54:19 +0100 Subject: Fewer html_safes --- app/helpers/diff_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 0b4fda12788..f9bacc8ba45 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -2,8 +2,8 @@ module DiffHelper def mark_inline_diffs(old_line, new_line) old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_line, new_line).inline_diffs - marked_old_line = Gitlab::Diff::InlineDiffMarker.new(old_line).mark(old_diffs).html_safe - marked_new_line = Gitlab::Diff::InlineDiffMarker.new(new_line).mark(new_diffs).html_safe + marked_old_line = Gitlab::Diff::InlineDiffMarker.new(old_line).mark(old_diffs) + marked_new_line = Gitlab::Diff::InlineDiffMarker.new(new_line).mark(new_diffs) [marked_old_line, marked_new_line] end -- cgit v1.2.1 From 7d9856313dea9e8b6a957caae5d2dba7c9756a80 Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Sat, 30 Jan 2016 15:39:33 -0800 Subject: Link to Thoughtbot review guidelines. --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1bd91f0bdce..e7659b06c71 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -255,6 +255,8 @@ For examples of feedback on merge requests please look at already request feel free to mention one of the Merge Marshalls of the [core team][]. Please ensure that your merge request meets the contribution acceptance criteria. +When having your code reviewed and when reviewing merge requests please take the [thoughtbot code review guidelines](https://github.com/thoughtbot/guides/tree/master/code-review) into account. + ## Definition of done If you contribute to GitLab please know that changes involve more than just -- cgit v1.2.1 From 3a95c427fddbb0679805d7107d6bb5dfe140cbc3 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 31 Jan 2016 19:25:00 -0500 Subject: Update CHANGELOG [ci skip] --- CHANGELOG | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 61c4f1f8903..cae3eeab54e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -21,7 +21,12 @@ v 8.5.0 (unreleased) - Mark inline difference between old and new paths when a file is renamed v 8.4.3 - - Increase lfs_objects size column to 8-byte integer to allow files larger than 2.1GB + - Increase lfs_objects size column to 8-byte integer to allow files larger + than 2.1GB + - Correctly highlight MR diff when MR has merge conflicts + - Fix highlighting in blame view + - Update sentry-raven gem to prevent "Not a git repository" console output + when running certain commands v 8.4.2 - Bump required gitlab-workhorse version to bring in a fix for missing @@ -32,7 +37,6 @@ v 8.4.2 improvement when checking if a repository was empty - Add instrumentation for Gitlab::Git::Repository instance methods so we can track them in Performance Monitoring. - - Correctly highlight MR diff when MR has merge conflicts - Increase contrast between highlighted code comments and inline diff marker - Fix method undefined when using external commit status in builds - Fix highlighting in blame view. -- cgit v1.2.1 From 3986f4f0831e5a4d6279b59cc184c59f6f9aa24b Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 31 Jan 2016 20:34:51 -0500 Subject: Make "Two-factor" casing consistent throughout the application --- CHANGELOG | 2 +- app/views/admin/application_settings/_form.html.haml | 6 +++--- app/views/profiles/two_factor_auths/new.html.haml | 2 +- doc/security/README.md | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index cae3eeab54e..58efbe2db7b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -91,7 +91,7 @@ v 8.4.0 - Show 'All' tab by default in the builds page - Add Open Graph and Twitter Card data to all pages - Fix API project lookups when querying with a namespace with dots (Stan Hu) - - Enable forcing Two-Factor authentication sitewide, with optional grace period + - Enable forcing Two-factor authentication sitewide, with optional grace period - Import GitHub Pull Requests into GitLab - Change single user API endpoint to return more detailed data (Michael Potthoff) - Update version check images to use SVG diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index c4fb2accdd0..b0f1a34cbec 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -105,14 +105,14 @@ = f.check_box :signin_enabled Sign-in enabled .form-group - = f.label :two_factor_authentication, 'Two-Factor authentication', class: 'control-label col-sm-2' + = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' .col-sm-10 .checkbox = f.label :require_two_factor_authentication do = f.check_box :require_two_factor_authentication - Require all users to setup Two-Factor authentication + Require all users to setup Two-factor authentication .form-group - = f.label :two_factor_authentication, 'Two-Factor grace period (hours)', class: 'control-label col-sm-2' + = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'control-label col-sm-2' .col-sm-10 = f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0' .help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml index 1a5b6efce35..b2830aa0834 100644 --- a/app/views/profiles/two_factor_auths/new.html.haml +++ b/app/views/profiles/two_factor_auths/new.html.haml @@ -1,6 +1,6 @@ - page_title 'Two-factor Authentication', 'Account' -%h2.page-title Two-Factor Authentication (2FA) +%h2.page-title Two-factor Authentication (2FA) %p Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code. diff --git a/doc/security/README.md b/doc/security/README.md index f34c792d005..be1abb88c3d 100644 --- a/doc/security/README.md +++ b/doc/security/README.md @@ -7,4 +7,4 @@ - [Reset your root password](reset_root_password.md) - [User File Uploads](user_file_uploads.md) - [How we manage the CRIME vulnerability](crime_vulnerability.md) -- [Enforce Two-Factor authentication](two_factor_authentication.md) +- [Enforce Two-factor authentication](two_factor_authentication.md) -- cgit v1.2.1