From 76aea978c6b2d8c69881836408c4947d816d2db2 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 9 Jun 2016 14:48:40 +0200 Subject: Add class that encapsulates error in new Ci config --- lib/gitlab/ci/config.rb | 10 ++++++++-- lib/gitlab/ci/config/node/configurable.rb | 4 ++-- lib/gitlab/ci/config/node/entry.rb | 9 +++++++++ lib/gitlab/ci/config/node/error.rb | 26 ++++++++++++++++++++++++++ lib/gitlab/ci/config/node/factory.rb | 1 + lib/gitlab/ci/config/node/script.rb | 2 +- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 2 +- spec/lib/gitlab/ci/config/node/error_spec.rb | 23 +++++++++++++++++++++++ spec/lib/gitlab/ci/config/node/factory_spec.rb | 10 ++++++++++ spec/lib/gitlab/ci/config/node/global_spec.rb | 8 +++++++- spec/lib/gitlab/ci/config/node/script_spec.rb | 2 +- spec/lib/gitlab/ci/config_spec.rb | 6 ++++++ 12 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 lib/gitlab/ci/config/node/error.rb create mode 100644 spec/lib/gitlab/ci/config/node/error_spec.rb diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index b48d3592f16..26028547fa0 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -4,8 +4,6 @@ module Gitlab # Base GitLab CI Configuration facade # class Config - delegate :valid?, :errors, to: :@global - ## # Temporary delegations that should be removed after refactoring # @@ -18,6 +16,14 @@ module Gitlab @global.process! end + def valid? + errors.none? + end + + def errors + @global.errors.map(&:to_s) + end + def to_hash @config end diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb index d60f87f3f94..2f4abbf9ffe 100644 --- a/lib/gitlab/ci/config/node/configurable.rb +++ b/lib/gitlab/ci/config/node/configurable.rb @@ -24,12 +24,12 @@ module Gitlab def prevalidate! unless @value.is_a?(Hash) - @errors << 'should be a configuration entry with hash value' + add_error('should be a configuration entry with hash value') end end def create_node(key, factory) - factory.with(value: @value[key]) + factory.with(value: @value[key], key: key) factory.nullify! unless @value.has_key?(key) factory.create! end diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb index 52758a962f3..f41e191d611 100644 --- a/lib/gitlab/ci/config/node/entry.rb +++ b/lib/gitlab/ci/config/node/entry.rb @@ -8,6 +8,7 @@ module Gitlab class Entry class InvalidError < StandardError; end + attr_writer :key attr_accessor :description def initialize(value) @@ -40,10 +41,18 @@ module Gitlab allowed_nodes.none? end + def key + @key || self.class.name.demodulize.underscore + end + def errors @errors + nodes.map(&:errors).flatten end + def add_error(message) + @errors << Error.new(message, self) + end + def allowed_nodes {} end diff --git a/lib/gitlab/ci/config/node/error.rb b/lib/gitlab/ci/config/node/error.rb new file mode 100644 index 00000000000..5b8d0288e4e --- /dev/null +++ b/lib/gitlab/ci/config/node/error.rb @@ -0,0 +1,26 @@ +module Gitlab + module Ci + class Config + module Node + class Error + def initialize(message, parent) + @message = message + @parent = parent + end + + def key + @parent.key + end + + def to_s + "#{key}: #{@message}" + end + + def ==(other) + other.to_s == to_s + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/node/factory.rb b/lib/gitlab/ci/config/node/factory.rb index 787ca006f5a..025ae40ef94 100644 --- a/lib/gitlab/ci/config/node/factory.rb +++ b/lib/gitlab/ci/config/node/factory.rb @@ -30,6 +30,7 @@ module Gitlab @entry_class.new(@attributes[:value]).tap do |entry| entry.description = @attributes[:description] + entry.key = @attributes[:key] end end end diff --git a/lib/gitlab/ci/config/node/script.rb b/lib/gitlab/ci/config/node/script.rb index 5072bf0db7d..314877a035c 100644 --- a/lib/gitlab/ci/config/node/script.rb +++ b/lib/gitlab/ci/config/node/script.rb @@ -19,7 +19,7 @@ module Gitlab def validate! unless validate_array_of_strings(@value) - @errors << 'before_script should be an array of strings' + add_error('should be an array of strings') end end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 5e1d2b8e4f5..40ee1eb64ca 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -820,7 +820,7 @@ EOT config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script: should be an array of strings") end it "returns errors if job before_script parameter is not an array of strings" do diff --git a/spec/lib/gitlab/ci/config/node/error_spec.rb b/spec/lib/gitlab/ci/config/node/error_spec.rb new file mode 100644 index 00000000000..764fdfdb821 --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/error_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Error do + let(:error) { described_class.new(message, parent) } + let(:message) { 'some error' } + let(:parent) { spy('parent') } + + before do + allow(parent).to receive(:key).and_return('parent_key') + end + + describe '#key' do + it 'returns underscored class name' do + expect(error.key).to eq 'parent_key' + end + end + + describe '#to_s' do + it 'returns valid error message' do + expect(error.to_s).to eq 'parent_key: some error' + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/factory_spec.rb b/spec/lib/gitlab/ci/config/node/factory_spec.rb index d681aa32456..01a707a6bd4 100644 --- a/spec/lib/gitlab/ci/config/node/factory_spec.rb +++ b/spec/lib/gitlab/ci/config/node/factory_spec.rb @@ -25,6 +25,16 @@ describe Gitlab::Ci::Config::Node::Factory do expect(entry.description).to eq 'test description' end end + + context 'when setting key' do + it 'creates entry with custom key' do + entry = factory + .with(value: ['ls', 'pwd'], key: 'test key') + .create! + + expect(entry.key).to eq 'test key' + end + end end context 'when not setting value' do diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb index b1972172435..89a7183b655 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/node/global_spec.rb @@ -13,6 +13,12 @@ describe Gitlab::Ci::Config::Node::Global do end end + describe '#key' do + it 'returns underscored class name' do + expect(global.key).to eq 'global' + end + end + context 'when hash is valid' do let(:hash) do { before_script: ['ls', 'pwd'] } @@ -79,7 +85,7 @@ describe Gitlab::Ci::Config::Node::Global do describe '#errors' do it 'reports errors from child nodes' do expect(global.errors) - .to include 'before_script should be an array of strings' + .to include 'before_script: should be an array of strings' end end diff --git a/spec/lib/gitlab/ci/config/node/script_spec.rb b/spec/lib/gitlab/ci/config/node/script_spec.rb index e4d6481f8a5..ff7016046cc 100644 --- a/spec/lib/gitlab/ci/config/node/script_spec.rb +++ b/spec/lib/gitlab/ci/config/node/script_spec.rb @@ -34,7 +34,7 @@ describe Gitlab::Ci::Config::Node::Script do describe '#errors' do it 'saves errors' do expect(entry.errors) - .to include /should be an array of strings/ + .to include 'script: should be an array of strings' end end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index 3871d939feb..2a5d132db7b 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -67,6 +67,12 @@ describe Gitlab::Ci::Config do expect(config.errors).not_to be_empty end end + + describe '#errors' do + it 'returns an array of strings' do + expect(config.errors).to all(be_an_instance_of(String)) + end + end end end end -- cgit v1.2.1 From 95520dfc72770955ae99c73f90e02037f3b1d4b5 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 16 Jun 2016 14:16:33 +0200 Subject: Add prototype of CI config node validator This makes use of `ActiveModel::Validations` encapsulated in a separate class that is accessible from a node object. --- lib/gitlab/ci/config.rb | 4 +-- lib/gitlab/ci/config/node/configurable.rb | 23 +++++++----- lib/gitlab/ci/config/node/entry.rb | 41 +++++++++------------- lib/gitlab/ci/config/node/error.rb | 26 -------------- lib/gitlab/ci/config/node/script.rb | 18 ++++++---- lib/gitlab/ci/config/node/validatable.rb | 30 ++++++++++++++++ lib/gitlab/ci/config/node/validator.rb | 25 +++++++++++++ spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 2 +- .../lib/gitlab/ci/config/node/configurable_spec.rb | 1 + spec/lib/gitlab/ci/config/node/error_spec.rb | 23 ------------ spec/lib/gitlab/ci/config/node/global_spec.rb | 2 +- spec/lib/gitlab/ci/config/node/script_spec.rb | 14 ++++---- 12 files changed, 108 insertions(+), 101 deletions(-) delete mode 100644 lib/gitlab/ci/config/node/error.rb create mode 100644 lib/gitlab/ci/config/node/validatable.rb create mode 100644 lib/gitlab/ci/config/node/validator.rb delete mode 100644 spec/lib/gitlab/ci/config/node/error_spec.rb diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index 26028547fa0..adfd097736e 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -17,11 +17,11 @@ module Gitlab end def valid? - errors.none? + @global.valid? end def errors - @global.errors.map(&:to_s) + @global.errors end def to_hash diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb index 2f4abbf9ffe..23d3f9f3277 100644 --- a/lib/gitlab/ci/config/node/configurable.rb +++ b/lib/gitlab/ci/config/node/configurable.rb @@ -16,21 +16,27 @@ module Gitlab module Configurable extend ActiveSupport::Concern + included do + validations do + validate :hash_config_value + + def hash_config_value + unless self.config.is_a?(Hash) + errors.add(:config, 'should be a configuration entry hash') + end + end + end + end + def allowed_nodes self.class.allowed_nodes || {} end private - def prevalidate! - unless @value.is_a?(Hash) - add_error('should be a configuration entry with hash value') - end - end - def create_node(key, factory) - factory.with(value: @value[key], key: key) - factory.nullify! unless @value.has_key?(key) + factory.with(value: @config[key], key: key) + factory.nullify! unless @config.has_key?(key) factory.create! end @@ -47,7 +53,6 @@ module Gitlab define_method(symbol) do raise Entry::InvalidError unless valid? - @nodes[symbol].try(:value) end diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb index f41e191d611..823c8bb5d13 100644 --- a/lib/gitlab/ci/config/node/entry.rb +++ b/lib/gitlab/ci/config/node/entry.rb @@ -7,16 +7,15 @@ module Gitlab # class Entry class InvalidError < StandardError; end + include Validatable - attr_writer :key - attr_accessor :description + attr_reader :config + attr_accessor :key, :description - def initialize(value) - @value = value + def initialize(config) + @config = config @nodes = {} - @errors = [] - - prevalidate! + @validator = self.class.validator.new(self) end def process! @@ -24,19 +23,13 @@ module Gitlab return unless valid? compose! - - nodes.each(&:process!) - nodes.each(&:validate!) + process_nodes! end def nodes @nodes.values end - def valid? - errors.none? - end - def leaf? allowed_nodes.none? end @@ -45,37 +38,35 @@ module Gitlab @key || self.class.name.demodulize.underscore end - def errors - @errors + nodes.map(&:errors).flatten + def valid? + errors.none? end - def add_error(message) - @errors << Error.new(message, self) + def errors + @validator.full_errors + + nodes.map(&:errors).flatten end def allowed_nodes {} end - def validate! - raise NotImplementedError - end - def value raise NotImplementedError end private - def prevalidate! - end - def compose! allowed_nodes.each do |key, essence| @nodes[key] = create_node(key, essence) end end + def process_nodes! + nodes.each(&:process!) + end + def create_node(key, essence) raise NotImplementedError end diff --git a/lib/gitlab/ci/config/node/error.rb b/lib/gitlab/ci/config/node/error.rb deleted file mode 100644 index 5b8d0288e4e..00000000000 --- a/lib/gitlab/ci/config/node/error.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Gitlab - module Ci - class Config - module Node - class Error - def initialize(message, parent) - @message = message - @parent = parent - end - - def key - @parent.key - end - - def to_s - "#{key}: #{@message}" - end - - def ==(other) - other.to_s == to_s - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/node/script.rb b/lib/gitlab/ci/config/node/script.rb index 314877a035c..18592acdac6 100644 --- a/lib/gitlab/ci/config/node/script.rb +++ b/lib/gitlab/ci/config/node/script.rb @@ -11,17 +11,21 @@ module Gitlab # implementation in Runner. # class Script < Entry - include ValidationHelpers + validations do + include ValidationHelpers - def value - @value.join("\n") - end + validate :array_of_strings - def validate! - unless validate_array_of_strings(@value) - add_error('should be an array of strings') + def array_of_strings + unless validate_array_of_strings(self.config) + errors.add(:config, 'should be an array of strings') + end end end + + def value + @config.join("\n") + end end end end diff --git a/lib/gitlab/ci/config/node/validatable.rb b/lib/gitlab/ci/config/node/validatable.rb new file mode 100644 index 00000000000..a0617d21ad8 --- /dev/null +++ b/lib/gitlab/ci/config/node/validatable.rb @@ -0,0 +1,30 @@ +module Gitlab + module Ci + class Config + module Node + module Validatable + extend ActiveSupport::Concern + + class_methods do + def validator + validator = Class.new(Node::Validator) + validator.include(ActiveModel::Validations) + + if defined?(@validations) + @validations.each { |rules| validator.class_eval(&rules) } + end + + validator + end + + private + + def validations(&block) + (@validations ||= []).append(block) + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/node/validator.rb b/lib/gitlab/ci/config/node/validator.rb new file mode 100644 index 00000000000..891b19c9743 --- /dev/null +++ b/lib/gitlab/ci/config/node/validator.rb @@ -0,0 +1,25 @@ +module Gitlab + module Ci + class Config + module Node + class Validator < SimpleDelegator + def initialize(node) + @node = node + super(node) + validate + end + + def full_errors + errors.full_messages.map do |error| + "#{@node.key} #{error}".humanize + end + end + + def self.name + 'Validator' + end + end + end + end + end +end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 40ee1eb64ca..42cbb555694 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -820,7 +820,7 @@ EOT config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script: should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Before script config should be an array of strings") end it "returns errors if job before_script parameter is not an array of strings" do diff --git a/spec/lib/gitlab/ci/config/node/configurable_spec.rb b/spec/lib/gitlab/ci/config/node/configurable_spec.rb index 47c68f96dc8..8ec7919f4d4 100644 --- a/spec/lib/gitlab/ci/config/node/configurable_spec.rb +++ b/spec/lib/gitlab/ci/config/node/configurable_spec.rb @@ -4,6 +4,7 @@ describe Gitlab::Ci::Config::Node::Configurable do let(:node) { Class.new } before do + node.include(Gitlab::Ci::Config::Node::Validatable) node.include(described_class) end diff --git a/spec/lib/gitlab/ci/config/node/error_spec.rb b/spec/lib/gitlab/ci/config/node/error_spec.rb deleted file mode 100644 index 764fdfdb821..00000000000 --- a/spec/lib/gitlab/ci/config/node/error_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Ci::Config::Node::Error do - let(:error) { described_class.new(message, parent) } - let(:message) { 'some error' } - let(:parent) { spy('parent') } - - before do - allow(parent).to receive(:key).and_return('parent_key') - end - - describe '#key' do - it 'returns underscored class name' do - expect(error.key).to eq 'parent_key' - end - end - - describe '#to_s' do - it 'returns valid error message' do - expect(error.to_s).to eq 'parent_key: some error' - end - end -end diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb index 89a7183b655..8bb76cf920a 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/node/global_spec.rb @@ -85,7 +85,7 @@ describe Gitlab::Ci::Config::Node::Global do describe '#errors' do it 'reports errors from child nodes' do expect(global.errors) - .to include 'before_script: should be an array of strings' + .to include 'Before script config should be an array of strings' end end diff --git a/spec/lib/gitlab/ci/config/node/script_spec.rb b/spec/lib/gitlab/ci/config/node/script_spec.rb index ff7016046cc..6af6aa15eef 100644 --- a/spec/lib/gitlab/ci/config/node/script_spec.rb +++ b/spec/lib/gitlab/ci/config/node/script_spec.rb @@ -1,13 +1,13 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Script do - let(:entry) { described_class.new(value) } + let(:entry) { described_class.new(config) } - describe '#validate!' do - before { entry.validate! } + describe '#process!' do + before { entry.process! } - context 'when entry value is correct' do - let(:value) { ['ls', 'pwd'] } + context 'when entry config value is correct' do + let(:config) { ['ls', 'pwd'] } describe '#value' do it 'returns concatenated command' do @@ -29,12 +29,12 @@ describe Gitlab::Ci::Config::Node::Script do end context 'when entry value is not correct' do - let(:value) { 'ls' } + let(:config) { 'ls' } describe '#errors' do it 'saves errors' do expect(entry.errors) - .to include 'script: should be an array of strings' + .to include 'Script config should be an array of strings' end end -- cgit v1.2.1 From 002e6ed1f042852bef78c3a749e13567d7a0ee5a Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 17 Jun 2016 09:33:32 +0200 Subject: Improve CI config entries validations prototype --- lib/gitlab/ci/config/node/configurable.rb | 7 +-- lib/gitlab/ci/config/node/entry.rb | 16 ++++-- lib/gitlab/ci/config/node/script.rb | 2 + lib/gitlab/ci/config/node/validatable.rb | 1 - lib/gitlab/ci/config/node/validator.rb | 5 +- .../lib/gitlab/ci/config/node/configurable_spec.rb | 15 +++-- spec/lib/gitlab/ci/config/node/global_spec.rb | 6 +- spec/lib/gitlab/ci/config/node/validatable_spec.rb | 50 ++++++++++++++++ spec/lib/gitlab/ci/config/node/validator_spec.rb | 67 ++++++++++++++++++++++ 9 files changed, 144 insertions(+), 25 deletions(-) create mode 100644 spec/lib/gitlab/ci/config/node/validatable_spec.rb create mode 100644 spec/lib/gitlab/ci/config/node/validator_spec.rb diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb index 23d3f9f3277..7b428c1362c 100644 --- a/lib/gitlab/ci/config/node/configurable.rb +++ b/lib/gitlab/ci/config/node/configurable.rb @@ -15,6 +15,7 @@ module Gitlab # module Configurable extend ActiveSupport::Concern + include Validatable included do validations do @@ -28,10 +29,6 @@ module Gitlab end end - def allowed_nodes - self.class.allowed_nodes || {} - end - private def create_node(key, factory) @@ -41,7 +38,7 @@ module Gitlab end class_methods do - def allowed_nodes + def nodes Hash[@allowed_nodes.map { |key, factory| [key, factory.dup] }] end diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb index 823c8bb5d13..f044ef965e9 100644 --- a/lib/gitlab/ci/config/node/entry.rb +++ b/lib/gitlab/ci/config/node/entry.rb @@ -7,7 +7,6 @@ module Gitlab # class Entry class InvalidError < StandardError; end - include Validatable attr_reader :config attr_accessor :key, :description @@ -16,6 +15,7 @@ module Gitlab @config = config @nodes = {} @validator = self.class.validator.new(self) + @validator.validate end def process! @@ -31,7 +31,7 @@ module Gitlab end def leaf? - allowed_nodes.none? + self.class.nodes.none? end def key @@ -47,18 +47,22 @@ module Gitlab nodes.map(&:errors).flatten end - def allowed_nodes + def value + raise NotImplementedError + end + + def self.nodes {} end - def value - raise NotImplementedError + def self.validator + Validator end private def compose! - allowed_nodes.each do |key, essence| + self.class.nodes.each do |key, essence| @nodes[key] = create_node(key, essence) end end diff --git a/lib/gitlab/ci/config/node/script.rb b/lib/gitlab/ci/config/node/script.rb index 18592acdac6..059650d8a52 100644 --- a/lib/gitlab/ci/config/node/script.rb +++ b/lib/gitlab/ci/config/node/script.rb @@ -11,6 +11,8 @@ module Gitlab # implementation in Runner. # class Script < Entry + include Validatable + validations do include ValidationHelpers diff --git a/lib/gitlab/ci/config/node/validatable.rb b/lib/gitlab/ci/config/node/validatable.rb index a0617d21ad8..f6e2896dfb2 100644 --- a/lib/gitlab/ci/config/node/validatable.rb +++ b/lib/gitlab/ci/config/node/validatable.rb @@ -8,7 +8,6 @@ module Gitlab class_methods do def validator validator = Class.new(Node::Validator) - validator.include(ActiveModel::Validations) if defined?(@validations) @validations.each { |rules| validator.class_eval(&rules) } diff --git a/lib/gitlab/ci/config/node/validator.rb b/lib/gitlab/ci/config/node/validator.rb index 891b19c9743..2454cc6d957 100644 --- a/lib/gitlab/ci/config/node/validator.rb +++ b/lib/gitlab/ci/config/node/validator.rb @@ -3,10 +3,11 @@ module Gitlab class Config module Node class Validator < SimpleDelegator + include ActiveModel::Validations + def initialize(node) - @node = node super(node) - validate + @node = node end def full_errors diff --git a/spec/lib/gitlab/ci/config/node/configurable_spec.rb b/spec/lib/gitlab/ci/config/node/configurable_spec.rb index 8ec7919f4d4..9bbda6e7396 100644 --- a/spec/lib/gitlab/ci/config/node/configurable_spec.rb +++ b/spec/lib/gitlab/ci/config/node/configurable_spec.rb @@ -4,30 +4,29 @@ describe Gitlab::Ci::Config::Node::Configurable do let(:node) { Class.new } before do - node.include(Gitlab::Ci::Config::Node::Validatable) node.include(described_class) end - describe 'allowed nodes' do + describe 'configured nodes' do before do node.class_eval do allow_node :object, Object, description: 'test object' end end - describe '#allowed_nodes' do - it 'has valid allowed nodes' do - expect(node.allowed_nodes).to include :object + describe '.nodes' do + it 'has valid nodes' do + expect(node.nodes).to include :object end it 'creates a node factory' do - expect(node.allowed_nodes[:object]) + expect(node.nodes[:object]) .to be_an_instance_of Gitlab::Ci::Config::Node::Factory end it 'returns a duplicated factory object' do - first_factory = node.allowed_nodes[:object] - second_factory = node.allowed_nodes[:object] + first_factory = node.nodes[:object] + second_factory = node.nodes[:object] expect(first_factory).not_to be_equal(second_factory) end diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb index 8bb76cf920a..fddd53a2b57 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/node/global_spec.rb @@ -3,13 +3,13 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Global do let(:global) { described_class.new(hash) } - describe '#allowed_nodes' do + describe '.nodes' do it 'can contain global config keys' do - expect(global.allowed_nodes).to include :before_script + expect(described_class.nodes).to include :before_script end it 'returns a hash' do - expect(global.allowed_nodes).to be_a Hash + expect(described_class.nodes).to be_a Hash end end diff --git a/spec/lib/gitlab/ci/config/node/validatable_spec.rb b/spec/lib/gitlab/ci/config/node/validatable_spec.rb new file mode 100644 index 00000000000..10cd01afcd1 --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/validatable_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Validatable do + let(:node) { Class.new } + + before do + node.include(described_class) + end + + describe '.validator' do + before do + node.class_eval do + attr_accessor :test_attribute + + validations do + validates :test_attribute, presence: true + end + end + end + + it 'returns validator' do + expect(node.validator.superclass) + .to be Gitlab::Ci::Config::Node::Validator + end + + context 'when validating node instance' do + let(:node_instance) { node.new } + + context 'when attribute is valid' do + before do + node_instance.test_attribute = 'valid' + end + + it 'instance of validator is valid' do + expect(node.validator.new(node_instance)).to be_valid + end + end + + context 'when attribute is not valid' do + before do + node_instance.test_attribute = nil + end + + it 'instance of validator is invalid' do + expect(node.validator.new(node_instance)).to be_invalid + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/validator_spec.rb b/spec/lib/gitlab/ci/config/node/validator_spec.rb new file mode 100644 index 00000000000..ad875d55384 --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/validator_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Validator do + let(:validator) { Class.new(described_class) } + let(:validator_instance) { validator.new(node) } + let(:node) { spy('node') } + + shared_examples 'delegated validator' do + context 'when node is valid' do + before do + allow(node).to receive(:test_attribute).and_return('valid value') + end + + it 'validates attribute in node' do + expect(node).to receive(:test_attribute) + expect(validator_instance).to be_valid + end + + it 'returns no errors' do + validator_instance.validate + + expect(validator_instance.full_errors).to be_empty + end + end + + context 'when node is invalid' do + before do + allow(node).to receive(:test_attribute).and_return(nil) + end + + it 'validates attribute in node' do + expect(node).to receive(:test_attribute) + expect(validator_instance).to be_invalid + end + + it 'returns errors' do + validator_instance.validate + + expect(validator_instance.full_errors).not_to be_empty + end + end + end + + describe 'attributes validations' do + before do + validator.class_eval do + validates :test_attribute, presence: true + end + end + + it_behaves_like 'delegated validator' + end + + describe 'interface validations' do + before do + validator.class_eval do + validate do + unless @node.test_attribute == 'valid value' + errors.add(:test_attribute, 'invalid value') + end + end + end + end + + it_behaves_like 'delegated validator' + end +end -- cgit v1.2.1 From a9bd16bd0a44332133d0f9fd859d9ffaba9e262f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 17 Jun 2016 11:55:55 +0200 Subject: Rename legacy validation helpers for new ci config --- lib/ci/gitlab_ci_yaml_processor.rb | 2 +- .../ci/config/node/legacy_validation_helpers.rb | 34 ++++++++++++++++++++++ lib/gitlab/ci/config/node/script.rb | 2 +- lib/gitlab/ci/config/node/validation_helpers.rb | 34 ---------------------- 4 files changed, 36 insertions(+), 36 deletions(-) create mode 100644 lib/gitlab/ci/config/node/legacy_validation_helpers.rb delete mode 100644 lib/gitlab/ci/config/node/validation_helpers.rb diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index e0b89cead06..1a5be9dbc46 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -2,7 +2,7 @@ module Ci class GitlabCiYamlProcessor class ValidationError < StandardError; end - include Gitlab::Ci::Config::Node::ValidationHelpers + include Gitlab::Ci::Config::Node::LegacyValidationHelpers DEFAULT_STAGES = %w(build test deploy) DEFAULT_STAGE = 'test' diff --git a/lib/gitlab/ci/config/node/legacy_validation_helpers.rb b/lib/gitlab/ci/config/node/legacy_validation_helpers.rb new file mode 100644 index 00000000000..970763670c5 --- /dev/null +++ b/lib/gitlab/ci/config/node/legacy_validation_helpers.rb @@ -0,0 +1,34 @@ +module Gitlab + module Ci + class Config + module Node + module LegacyValidationHelpers + private + + def validate_duration(value) + value.is_a?(String) && ChronicDuration.parse(value) + rescue ChronicDuration::DurationParseError + false + end + + def validate_array_of_strings(values) + values.is_a?(Array) && values.all? { |value| validate_string(value) } + end + + def validate_variables(variables) + variables.is_a?(Hash) && + variables.all? { |key, value| validate_string(key) && validate_string(value) } + end + + def validate_string(value) + value.is_a?(String) || value.is_a?(Symbol) + end + + def validate_boolean(value) + value.in?([true, false]) + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/node/script.rb b/lib/gitlab/ci/config/node/script.rb index 059650d8a52..44490096f28 100644 --- a/lib/gitlab/ci/config/node/script.rb +++ b/lib/gitlab/ci/config/node/script.rb @@ -14,7 +14,7 @@ module Gitlab include Validatable validations do - include ValidationHelpers + include LegacyValidationHelpers validate :array_of_strings diff --git a/lib/gitlab/ci/config/node/validation_helpers.rb b/lib/gitlab/ci/config/node/validation_helpers.rb deleted file mode 100644 index 42ef60244ba..00000000000 --- a/lib/gitlab/ci/config/node/validation_helpers.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Gitlab - module Ci - class Config - module Node - module ValidationHelpers - private - - def validate_duration(value) - value.is_a?(String) && ChronicDuration.parse(value) - rescue ChronicDuration::DurationParseError - false - end - - def validate_array_of_strings(values) - values.is_a?(Array) && values.all? { |value| validate_string(value) } - end - - def validate_variables(variables) - variables.is_a?(Hash) && - variables.all? { |key, value| validate_string(key) && validate_string(value) } - end - - def validate_string(value) - value.is_a?(String) || value.is_a?(Symbol) - end - - def validate_boolean(value) - value.in?([true, false]) - end - end - end - end - end -end -- cgit v1.2.1 From d9ca84015c04d8836c09c3ebb70a8240262b60e8 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 17 Jun 2016 12:06:48 +0200 Subject: Add first custom validator for new ci config This follows a standard `ActiveModel` pattern of creating a custom validators. We use `ActiveModel::EachValidator` here that reuses methods provided by `LegacyValidationHelpers`. We will remove `LegacyValidationHelpers` on some point in the future, at the later stages of CI configuration refactoring. It may be possible to rewrite custom validators to use format like: `validates :config, array_of: String` --- lib/gitlab/ci/config/node/script.rb | 10 +--------- lib/gitlab/ci/config/node/validator.rb | 1 + lib/gitlab/ci/config/node/validators.rb | 19 +++++++++++++++++++ .../validators/array_of_strings_validator_spec.rb | 0 4 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 lib/gitlab/ci/config/node/validators.rb create mode 100644 spec/lib/gitlab/ci/config/node/validators/array_of_strings_validator_spec.rb diff --git a/lib/gitlab/ci/config/node/script.rb b/lib/gitlab/ci/config/node/script.rb index 44490096f28..c044f5c5e71 100644 --- a/lib/gitlab/ci/config/node/script.rb +++ b/lib/gitlab/ci/config/node/script.rb @@ -14,15 +14,7 @@ module Gitlab include Validatable validations do - include LegacyValidationHelpers - - validate :array_of_strings - - def array_of_strings - unless validate_array_of_strings(self.config) - errors.add(:config, 'should be an array of strings') - end - end + validates :config, array_of_strings: true end def value diff --git a/lib/gitlab/ci/config/node/validator.rb b/lib/gitlab/ci/config/node/validator.rb index 2454cc6d957..02edc9219c3 100644 --- a/lib/gitlab/ci/config/node/validator.rb +++ b/lib/gitlab/ci/config/node/validator.rb @@ -4,6 +4,7 @@ module Gitlab module Node class Validator < SimpleDelegator include ActiveModel::Validations + include Node::Validators def initialize(node) super(node) diff --git a/lib/gitlab/ci/config/node/validators.rb b/lib/gitlab/ci/config/node/validators.rb new file mode 100644 index 00000000000..36d48394a8c --- /dev/null +++ b/lib/gitlab/ci/config/node/validators.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + class Config + module Node + module Validators + class ArrayOfStringsValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + unless validate_array_of_strings(value) + record.errors.add(attribute, 'should be an array of strings') + end + end + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/validators/array_of_strings_validator_spec.rb b/spec/lib/gitlab/ci/config/node/validators/array_of_strings_validator_spec.rb new file mode 100644 index 00000000000..e69de29bb2d -- cgit v1.2.1 From 44b00a1ebbfeaa095343f55f6c12dcbc65b85924 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 17 Jun 2016 14:51:39 +0200 Subject: Extract CI entry config hash validation to validator --- lib/gitlab/ci/config/node/configurable.rb | 8 +------- lib/gitlab/ci/config/node/validators.rb | 8 ++++++++ .../ci/config/node/validators/array_of_strings_validator_spec.rb | 0 3 files changed, 9 insertions(+), 7 deletions(-) delete mode 100644 spec/lib/gitlab/ci/config/node/validators/array_of_strings_validator_spec.rb diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb index 7b428c1362c..374ff71d0f5 100644 --- a/lib/gitlab/ci/config/node/configurable.rb +++ b/lib/gitlab/ci/config/node/configurable.rb @@ -19,13 +19,7 @@ module Gitlab included do validations do - validate :hash_config_value - - def hash_config_value - unless self.config.is_a?(Hash) - errors.add(:config, 'should be a configuration entry hash') - end - end + validates :config, hash: true end end diff --git a/lib/gitlab/ci/config/node/validators.rb b/lib/gitlab/ci/config/node/validators.rb index 36d48394a8c..dc9cdb9a220 100644 --- a/lib/gitlab/ci/config/node/validators.rb +++ b/lib/gitlab/ci/config/node/validators.rb @@ -12,6 +12,14 @@ module Gitlab end end end + + class HashValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless value.is_a?(Hash) + record.errors.add(attribute, 'should be a configuration entry hash') + end + end + end end end end diff --git a/spec/lib/gitlab/ci/config/node/validators/array_of_strings_validator_spec.rb b/spec/lib/gitlab/ci/config/node/validators/array_of_strings_validator_spec.rb deleted file mode 100644 index e69de29bb2d..00000000000 -- cgit v1.2.1 From 69c70928c8b663e704fc9bdb21fd4094e3d27327 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 22 Jun 2016 14:10:24 -0700 Subject: Update default regexp documentation to match 8172d2c1. [ci skip] --- doc/customization/issue_closing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md index 194b8e00299..4620bb2dcde 100644 --- a/doc/customization/issue_closing.md +++ b/doc/customization/issue_closing.md @@ -8,7 +8,7 @@ the matched text will be closed. This happens when the commit is pushed to a pro When not specified, the default `issue_closing_pattern` as shown below will be used: ```bash -((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+) +((?:[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+))+) ``` Here, `%{issue_ref}` is a complex regular expression defined inside GitLab, that matches a reference to a local issue (`#123`), cross-project issue (`group/project#123`) or a link to an issue (`https://gitlab.example.com/group/project/issues/123`). -- cgit v1.2.1 From 6587feba65371a7417a7726b6a19681e99187ecd Mon Sep 17 00:00:00 2001 From: Marc Siegfriedt Date: Tue, 7 Jun 2016 22:01:54 +0000 Subject: gitlab-org/gitlab-ce#17818 - add api call for issues by group rely only on IssuesFinder docs and changelog --- CHANGELOG | 1 + doc/api/issues.md | 80 +++++++++++++++++++++- lib/api/issues.rb | 35 ++++++++++ spec/requests/api/issues_spec.rb | 142 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 256 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4e405478825..5cf7f3780ca 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ v 8.10.0 (unreleased) - Fix MR-auto-close text added to description. !4836 - Fix pagination when sorting by columns with lots of ties (like priority) - Implement Subresource Integrity for CSS and JavaScript assets. This prevents malicious assets from loading in the case of a CDN compromise. + - Add API endpoint for a group issues !4520 (mahcsig) v 8.9.1 - Fix GitLab project import issues related to notes and builds diff --git a/doc/api/issues.md b/doc/api/issues.md index 0bc82ef9edb..708fc691f67 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -28,7 +28,7 @@ GET /issues?labels=foo,bar&state=opened | 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 | +| `labels` | string | no | Comma-separated list of label names, issues with any of the labels will be returned | | `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` | @@ -83,6 +83,82 @@ Example response: ] ``` +## List group issues + +Get a list of a group's issues. + +``` +GET /groups/:id/issues +GET /groups/:id/issues?state=opened +GET /groups/:id/issues?state=closed +GET /groups/:id/issues?labels=foo +GET /groups/:id/issues?labels=foo,bar +GET /groups/:id/issues?labels=foo,bar&state=opened +GET /groups/:id/issues?milestone=1.0.0 +GET /groups/:id/issues?milestone=1.0.0&state=opened +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a group | +| `state` | string | no | Return all issues or just those that are `opened` or `closed`| +| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned | +| `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/groups/4/issues +``` + +Example response: + +```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", + "subscribed" : false, + "user_notes_count": 1 + } +] +``` + ## List project issues Get a list of a project's issues. @@ -104,7 +180,7 @@ GET /projects/:id/issues?iid=42 | `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 | +| `labels` | string | no | Comma-separated list of label names, issues with any of the labels will be returned | | `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` | diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 4c43257c48a..8a03a41e9c5 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -59,6 +59,41 @@ module API end end + resource :groups do + # Get a list of group issues + # + # Parameters: + # id (required) - The ID of a group + # state (optional) - Return "opened" or "closed" issues + # 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` + # + # Example Requests: + # GET /groups/:id/issues + # GET /groups/:id/issues?state=opened + # GET /groups/:id/issues?state=closed + # GET /groups/:id/issues?labels=foo + # GET /groups/:id/issues?labels=foo,bar + # GET /groups/:id/issues?labels=foo,bar&state=opened + # GET /groups/:id/issues?milestone=1.0.0 + # GET /groups/:id/issues?milestone=1.0.0&state=closed + get ":id/issues" do + group = find_group(params[:id]) + + params[:state] ||= 'opened' + params[:group_id] = group.id + params[:milestone_title] = params.delete(:milestone) + params[:label_name] = params.delete(:labels) + params[:sort] = "#{params.delete(:order_by)}_#{params.delete(:sort)}" if params[:order_by] && params[:sort] + + issues = IssuesFinder.new(current_user, params).execute + + present paginate(issues), with: Entities::Issue, current_user: current_user + end + end + resource :projects do # Get a list of project issues # diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 59e557c5b2a..edec53dad89 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -136,6 +136,148 @@ describe API::API, api: true do end end + describe "GET /groups/:id/issues" do + let!(:group) { create(:group) } + let!(:group_project) { create(:project, :public, creator_id: user.id, namespace: group) } + let!(:group_closed_issue) do + create :closed_issue, + author: user, + assignee: user, + project: group_project, + state: :closed, + milestone: group_milestone + end + let!(:group_confidential_issue) do + create :issue, + :confidential, + project: group_project, + author: author, + assignee: assignee + end + let!(:group_issue) do + create :issue, + author: user, + assignee: user, + project: group_project, + milestone: group_milestone + end + let!(:group_label) do + create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project) + end + let!(:group_label_link) { create(:label_link, label: group_label, target: group_issue) } + let!(:group_milestone) { create(:milestone, title: '3.0.0', project: group_project) } + let!(:group_empty_milestone) do + create(:milestone, title: '4.0.0', project: group_project) + end + let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) } + + before do + group_project.team << [user, :reporter] + end + let(:base_url) { "/groups/#{group.id}/issues" } + + it 'returns group issues without confidential issues for non project members' do + get api(base_url, non_member) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['title']).to eq(group_issue.title) + end + + it 'returns group confidential issues for author' do + get api(base_url, author) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + end + + it 'returns group confidential issues for assignee' do + get api(base_url, assignee) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + end + + it 'returns group issues with confidential issues for project members' do + get api(base_url, user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + end + + it 'returns group confidential issues for admin' do + get api(base_url, admin) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + end + + it 'returns an array of labeled group issues' do + get api("#{base_url}?labels=#{group_label.title}", user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([group_label.title]) + end + + it 'returns an array of labeled group issues where all labels match' do + get api("#{base_url}?labels=#{group_label.title},foo,bar", user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an empty array if no group issue matches labels' do + get api("#{base_url}?labels=foo,bar", user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an empty array if no issue matches milestone' do + get api("#{base_url}?milestone=#{group_empty_milestone.title}", user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an empty array if milestone does not exist' do + get api("#{base_url}?milestone=foo", user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an array of issues in given milestone' do + get api("#{base_url}?milestone=#{group_milestone.title}", user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(group_issue.id) + end + + it 'returns an array of issues matching state in milestone' do + get api("#{base_url}?milestone=#{group_milestone.title}"\ + '&state=closed', user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(group_closed_issue.id) + end + end + describe "GET /projects/:id/issues" do let(:base_url) { "/projects/#{project.id}" } let(:title) { milestone.title } -- cgit v1.2.1 From 7ce0968a52a5bc013e49865205a8998881884c02 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 23 Jun 2016 10:33:38 -0600 Subject: Re-implement page-specific JS in a better way. This makes larger libraries more cacheable and will allow us to use SRI with the dynamically included libraries. --- app/assets/javascripts/application.js.coffee | 2 +- app/assets/javascripts/graphs/application.js.coffee | 1 - app/assets/javascripts/lib/chart/application.js.coffee | 1 + app/assets/javascripts/lib/d3/application.js.coffee | 1 + app/assets/javascripts/lib/raphael/application.js.coffee | 3 +++ app/assets/javascripts/users/application.js.coffee | 6 ------ app/views/layouts/_head.html.haml | 7 ++----- app/views/projects/graphs/_head.html.haml | 4 +++- app/views/projects/network/show.html.haml | 4 +++- app/views/users/show.html.haml | 4 +++- 10 files changed, 17 insertions(+), 16 deletions(-) create mode 100644 app/assets/javascripts/lib/chart/application.js.coffee create mode 100644 app/assets/javascripts/lib/d3/application.js.coffee create mode 100644 app/assets/javascripts/lib/raphael/application.js.coffee diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 0206db461da..07b45eae9bb 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -50,7 +50,7 @@ #= require_directory ./ci #= require_directory ./commit #= require_directory ./extensions -#= require_directory ./lib +#= require_directory ./lib/. #= require_directory ./u2f #= require_directory . #= require fuzzaldrin-plus diff --git a/app/assets/javascripts/graphs/application.js.coffee b/app/assets/javascripts/graphs/application.js.coffee index 91f81a5d249..e0f681acf0b 100644 --- a/app/assets/javascripts/graphs/application.js.coffee +++ b/app/assets/javascripts/graphs/application.js.coffee @@ -4,5 +4,4 @@ # It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the # the compiled file. # -#= require Chart #= require_tree . diff --git a/app/assets/javascripts/lib/chart/application.js.coffee b/app/assets/javascripts/lib/chart/application.js.coffee new file mode 100644 index 00000000000..82217fc5107 --- /dev/null +++ b/app/assets/javascripts/lib/chart/application.js.coffee @@ -0,0 +1 @@ +#= require Chart diff --git a/app/assets/javascripts/lib/d3/application.js.coffee b/app/assets/javascripts/lib/d3/application.js.coffee new file mode 100644 index 00000000000..74f0a0bb06a --- /dev/null +++ b/app/assets/javascripts/lib/d3/application.js.coffee @@ -0,0 +1 @@ +#= require d3 diff --git a/app/assets/javascripts/lib/raphael/application.js.coffee b/app/assets/javascripts/lib/raphael/application.js.coffee new file mode 100644 index 00000000000..ab8e5979b87 --- /dev/null +++ b/app/assets/javascripts/lib/raphael/application.js.coffee @@ -0,0 +1,3 @@ +#= require raphael +#= require g.raphael +#= require g.bar diff --git a/app/assets/javascripts/users/application.js.coffee b/app/assets/javascripts/users/application.js.coffee index 647ffbf5f45..91cacfece46 100644 --- a/app/assets/javascripts/users/application.js.coffee +++ b/app/assets/javascripts/users/application.js.coffee @@ -1,8 +1,2 @@ -# This is a manifest file that'll be compiled into including all the files listed below. -# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically -# be included in the compiled file accessible from http://example.com/assets/application.js -# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -# the compiled file. # -#= require d3 #= require_tree . diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index d5965a6ec99..bc3118f70aa 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -30,11 +30,8 @@ = javascript_include_tag "application", integrity: true - -# FIXME: SRI doesn't apply to the dynamically-generated per-page - -# JavaScript due to a bug in sprockets-rails. - -# See https://github.com/rails/sprockets-rails/issues/359 - - if page_specific_javascripts - = javascript_include_tag page_specific_javascripts, {"data-turbolinks-track" => true} + - if :page_specific_javascripts + = yield :page_specific_javascripts = csrf_meta_tags diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index a388d9a0a61..5ea4adcd0fa 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -1,7 +1,9 @@ .nav-links.sub-nav %ul{ class: (container_class) } - - page_specific_javascripts asset_path("graphs/application.js") + - content_for :page_specific_javascripts do + = javascript_include_tag 'lib/chart/application.js', {integrity: true, "data-turbolinks-track" => true} + = javascript_include_tag 'graphs/application.js', {integrity: true, "data-turbolinks-track" => true} = nav_link(action: :show) do = link_to 'Contributors', namespace_project_graph_path = nav_link(action: :commits) do diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index 593af319a47..749afe6b6ab 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -1,5 +1,7 @@ - page_title "Network", @ref -- page_specific_javascripts asset_path("network/application.js") +- content_for :page_specific_javascripts do + = javascript_include_tag 'lib/raphael/application.js', {integrity: true, "data-turbolinks-track" => true} + = javascript_include_tag 'network/application.js', {integrity: true, "data-turbolinks-track" => true} = render "projects/commits/head" = render "head" %div{ class: (container_class) } diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 92305594a81..9ca351e5147 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,6 +1,8 @@ - page_title @user.name - page_description @user.bio -- page_specific_javascripts asset_path("users/application.js") +- content_for :page_specific_javascripts do + = javascript_include_tag 'lib/d3/application.js', {integrity: true, "data-turbolinks-track" => true} + = javascript_include_tag 'users/application.js', {integrity: true, "data-turbolinks-track" => true} - header_title @user.name, user_path(@user) - @no_container = true -- cgit v1.2.1 From 4cca9a3e0f388210883ea2e3be97e92ac0576b38 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 23 Jun 2016 10:55:40 -0600 Subject: Reorganize the lib directory. --- app/assets/javascripts/application.js.coffee | 2 +- app/assets/javascripts/lib/animate.js.coffee | 39 ---- app/assets/javascripts/lib/chart.js.coffee | 1 + .../javascripts/lib/chart/application.js.coffee | 1 - app/assets/javascripts/lib/common_utils.js.coffee | 65 ------- app/assets/javascripts/lib/d3.js.coffee | 1 + .../javascripts/lib/d3/application.js.coffee | 1 - .../javascripts/lib/datetime_utility.js.coffee | 24 --- .../javascripts/lib/emoji_aliases.js.coffee.erb | 2 - app/assets/javascripts/lib/jquery.timeago.js | 181 ------------------ app/assets/javascripts/lib/md5.js | 211 --------------------- app/assets/javascripts/lib/notify.js.coffee | 35 ---- app/assets/javascripts/lib/raphael.js.coffee | 3 + .../javascripts/lib/raphael/application.js.coffee | 3 - app/assets/javascripts/lib/text_utility.js.coffee | 79 -------- app/assets/javascripts/lib/type_utility.js.coffee | 9 - app/assets/javascripts/lib/url_utility.js.coffee | 52 ----- app/assets/javascripts/lib/utf8_encode.js | 70 ------- app/assets/javascripts/lib/utils/animate.js.coffee | 39 ++++ .../javascripts/lib/utils/common_utils.js.coffee | 65 +++++++ .../lib/utils/datetime_utility.js.coffee | 24 +++ .../lib/utils/emoji_aliases.js.coffee.erb | 2 + app/assets/javascripts/lib/utils/jquery.timeago.js | 181 ++++++++++++++++++ app/assets/javascripts/lib/utils/md5.js | 211 +++++++++++++++++++++ app/assets/javascripts/lib/utils/notify.js.coffee | 35 ++++ .../javascripts/lib/utils/text_utility.js.coffee | 79 ++++++++ .../javascripts/lib/utils/type_utility.js.coffee | 9 + .../javascripts/lib/utils/url_utility.js.coffee | 52 +++++ app/assets/javascripts/lib/utils/utf8_encode.js | 70 +++++++ .../javascripts/network/application.js.coffee | 3 - app/views/projects/graphs/_head.html.haml | 2 +- app/views/projects/network/show.html.haml | 2 +- app/views/users/show.html.haml | 2 +- 33 files changed, 776 insertions(+), 779 deletions(-) delete mode 100644 app/assets/javascripts/lib/animate.js.coffee create mode 100644 app/assets/javascripts/lib/chart.js.coffee delete mode 100644 app/assets/javascripts/lib/chart/application.js.coffee delete mode 100644 app/assets/javascripts/lib/common_utils.js.coffee create mode 100644 app/assets/javascripts/lib/d3.js.coffee delete mode 100644 app/assets/javascripts/lib/d3/application.js.coffee delete mode 100644 app/assets/javascripts/lib/datetime_utility.js.coffee delete mode 100644 app/assets/javascripts/lib/emoji_aliases.js.coffee.erb delete mode 100644 app/assets/javascripts/lib/jquery.timeago.js delete mode 100644 app/assets/javascripts/lib/md5.js delete mode 100644 app/assets/javascripts/lib/notify.js.coffee create mode 100644 app/assets/javascripts/lib/raphael.js.coffee delete mode 100644 app/assets/javascripts/lib/raphael/application.js.coffee delete mode 100644 app/assets/javascripts/lib/text_utility.js.coffee delete mode 100644 app/assets/javascripts/lib/type_utility.js.coffee delete mode 100644 app/assets/javascripts/lib/url_utility.js.coffee delete mode 100644 app/assets/javascripts/lib/utf8_encode.js create mode 100644 app/assets/javascripts/lib/utils/animate.js.coffee create mode 100644 app/assets/javascripts/lib/utils/common_utils.js.coffee create mode 100644 app/assets/javascripts/lib/utils/datetime_utility.js.coffee create mode 100644 app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb create mode 100644 app/assets/javascripts/lib/utils/jquery.timeago.js create mode 100644 app/assets/javascripts/lib/utils/md5.js create mode 100644 app/assets/javascripts/lib/utils/notify.js.coffee create mode 100644 app/assets/javascripts/lib/utils/text_utility.js.coffee create mode 100644 app/assets/javascripts/lib/utils/type_utility.js.coffee create mode 100644 app/assets/javascripts/lib/utils/url_utility.js.coffee create mode 100644 app/assets/javascripts/lib/utils/utf8_encode.js diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 07b45eae9bb..5c5a4ca7670 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -50,7 +50,7 @@ #= require_directory ./ci #= require_directory ./commit #= require_directory ./extensions -#= require_directory ./lib/. +#= require_directory ./lib/utils #= require_directory ./u2f #= require_directory . #= require fuzzaldrin-plus diff --git a/app/assets/javascripts/lib/animate.js.coffee b/app/assets/javascripts/lib/animate.js.coffee deleted file mode 100644 index ec3b44d6126..00000000000 --- a/app/assets/javascripts/lib/animate.js.coffee +++ /dev/null @@ -1,39 +0,0 @@ -((w) -> - if not w.gl? then w.gl = {} - if not gl.animate? then gl.animate = {} - - gl.animate.animate = ($el, animation, options, done) -> - if options?.cssStart? - $el.css(options.cssStart) - $el - .removeClass(animation + ' animated') - .addClass(animation + ' animated') - .one 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', -> - $(this).removeClass(animation + ' animated') - if done? - done() - if options?.cssEnd? - $el.css(options.cssEnd) - return - return - - gl.animate.animateEach = ($els, animation, time, options, done) -> - dfd = $.Deferred() - if not $els.length - dfd.resolve() - $els.each((i) -> - setTimeout(=> - $this = $(@) - gl.animate.animate($this, animation, options, => - if i is $els.length - 1 - dfd.resolve() - if done? - done() - ) - ,time * i - ) - return - ) - return dfd.promise() - return -) window \ No newline at end of file diff --git a/app/assets/javascripts/lib/chart.js.coffee b/app/assets/javascripts/lib/chart.js.coffee new file mode 100644 index 00000000000..82217fc5107 --- /dev/null +++ b/app/assets/javascripts/lib/chart.js.coffee @@ -0,0 +1 @@ +#= require Chart diff --git a/app/assets/javascripts/lib/chart/application.js.coffee b/app/assets/javascripts/lib/chart/application.js.coffee deleted file mode 100644 index 82217fc5107..00000000000 --- a/app/assets/javascripts/lib/chart/application.js.coffee +++ /dev/null @@ -1 +0,0 @@ -#= require Chart diff --git a/app/assets/javascripts/lib/common_utils.js.coffee b/app/assets/javascripts/lib/common_utils.js.coffee deleted file mode 100644 index e39dcb2daa9..00000000000 --- a/app/assets/javascripts/lib/common_utils.js.coffee +++ /dev/null @@ -1,65 +0,0 @@ -((w) -> - - w.gl or= {} - w.gl.utils or= {} - - w.gl.utils.isInGroupsPage = -> - - return $('body').data('page').split(':')[0] is 'groups' - - - w.gl.utils.isInProjectPage = -> - - return $('body').data('page').split(':')[0] is 'projects' - - - w.gl.utils.getProjectSlug = -> - - return if @isInProjectPage() then $('body').data 'project' else null - - - w.gl.utils.getGroupSlug = -> - - return if @isInGroupsPage() then $('body').data 'group' else null - - - - gl.utils.updateTooltipTitle = ($tooltipEl, newTitle) -> - - $tooltipEl - .tooltip 'destroy' - .attr 'title', newTitle - .tooltip 'fixTitle' - - - gl.utils.preventDisabledButtons = -> - - $('.btn').click (e) -> - if $(this).hasClass 'disabled' - e.preventDefault() - e.stopImmediatePropagation() - return false - - - jQuery.timefor = (time, suffix, expiredLabel) -> - - return '' unless time - - suffix or= 'remaining' - expiredLabel or= 'Past due' - - jQuery.timeago.settings.allowFuture = yes - - { suffixFromNow } = jQuery.timeago.settings.strings - jQuery.timeago.settings.strings.suffixFromNow = suffix - - timefor = $.timeago time - - if timefor.indexOf('ago') > -1 - timefor = expiredLabel - - jQuery.timeago.settings.strings.suffixFromNow = suffixFromNow - - return timefor - -) window diff --git a/app/assets/javascripts/lib/d3.js.coffee b/app/assets/javascripts/lib/d3.js.coffee new file mode 100644 index 00000000000..74f0a0bb06a --- /dev/null +++ b/app/assets/javascripts/lib/d3.js.coffee @@ -0,0 +1 @@ +#= require d3 diff --git a/app/assets/javascripts/lib/d3/application.js.coffee b/app/assets/javascripts/lib/d3/application.js.coffee deleted file mode 100644 index 74f0a0bb06a..00000000000 --- a/app/assets/javascripts/lib/d3/application.js.coffee +++ /dev/null @@ -1 +0,0 @@ -#= require d3 diff --git a/app/assets/javascripts/lib/datetime_utility.js.coffee b/app/assets/javascripts/lib/datetime_utility.js.coffee deleted file mode 100644 index 948d6dbf07e..00000000000 --- a/app/assets/javascripts/lib/datetime_utility.js.coffee +++ /dev/null @@ -1,24 +0,0 @@ -((w) -> - - w.gl ?= {} - w.gl.utils ?= {} - - w.gl.utils.formatDate = (datetime) -> - dateFormat(datetime, 'mmm d, yyyy h:MMtt Z') - - w.gl.utils.localTimeAgo = ($timeagoEls, setTimeago = true) -> - $timeagoEls.each( -> - $el = $(@) - $el.attr('title', gl.utils.formatDate($el.attr('datetime'))) - ) - - if setTimeago - $timeagoEls.timeago() - $timeagoEls.tooltip('destroy') - - # Recreate with custom template - $timeagoEls.tooltip( - template: '' - ) - -) window diff --git a/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb b/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb deleted file mode 100644 index 80f9936b9c2..00000000000 --- a/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb +++ /dev/null @@ -1,2 +0,0 @@ -gl.emojiAliases = -> - JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>') diff --git a/app/assets/javascripts/lib/jquery.timeago.js b/app/assets/javascripts/lib/jquery.timeago.js deleted file mode 100644 index cc17aa7d3d1..00000000000 --- a/app/assets/javascripts/lib/jquery.timeago.js +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Timeago is a jQuery plugin that makes it easy to support automatically - * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago"). - * - * @name timeago - * @version 1.1.0 - * @requires jQuery v1.2.3+ - * @author Ryan McGeary - * @license MIT License - http://www.opensource.org/licenses/mit-license.php - * - * For usage and examples, visit: - * http://timeago.yarp.com/ - * - * Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org) - */ - -(function (factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['jquery'], factory); - } else { - // Browser globals - factory(jQuery); - } -}(function ($) { - $.timeago = function(timestamp) { - if (timestamp instanceof Date) { - return inWords(timestamp); - } else if (typeof timestamp === "string") { - return inWords($.timeago.parse(timestamp)); - } else if (typeof timestamp === "number") { - return inWords(new Date(timestamp)); - } else { - return inWords($.timeago.datetime(timestamp)); - } - }; - var $t = $.timeago; - - $.extend($.timeago, { - settings: { - refreshMillis: 60000, - allowFuture: false, - strings: { - prefixAgo: null, - prefixFromNow: null, - suffixAgo: "ago", - suffixFromNow: "from now", - seconds: "less than a minute", - minute: "about a minute", - minutes: "%d minutes", - hour: "about an hour", - hours: "about %d hours", - day: "a day", - days: "%d days", - month: "about a month", - months: "%d months", - year: "about a year", - years: "%d years", - wordSeparator: " ", - numbers: [] - } - }, - inWords: function(distanceMillis) { - var $l = this.settings.strings; - var prefix = $l.prefixAgo; - var suffix = $l.suffixAgo; - if (this.settings.allowFuture) { - if (distanceMillis < 0) { - prefix = $l.prefixFromNow; - suffix = $l.suffixFromNow; - } - } - - var seconds = Math.abs(distanceMillis) / 1000; - var minutes = seconds / 60; - var hours = minutes / 60; - var days = hours / 24; - var years = days / 365; - - function substitute(stringOrFunction, number) { - var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction; - var value = ($l.numbers && $l.numbers[number]) || number; - return string.replace(/%d/i, value); - } - - var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) || - seconds < 90 && substitute($l.minute, 1) || - minutes < 45 && substitute($l.minutes, Math.round(minutes)) || - minutes < 90 && substitute($l.hour, 1) || - hours < 24 && substitute($l.hours, Math.round(hours)) || - hours < 42 && substitute($l.day, 1) || - days < 30 && substitute($l.days, Math.round(days)) || - days < 45 && substitute($l.month, 1) || - days < 365 && substitute($l.months, Math.round(days / 30)) || - years < 1.5 && substitute($l.year, 1) || - substitute($l.years, Math.round(years)); - - var separator = $l.wordSeparator || ""; - if ($l.wordSeparator === undefined) { separator = " "; } - return $.trim([prefix, words, suffix].join(separator)); - }, - parse: function(iso8601) { - var s = $.trim(iso8601); - s = s.replace(/\.\d+/,""); // remove milliseconds - s = s.replace(/-/,"/").replace(/-/,"/"); - s = s.replace(/T/," ").replace(/Z/," UTC"); - s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400 - return new Date(s); - }, - datetime: function(elem) { - var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title"); - return $t.parse(iso8601); - }, - isTime: function(elem) { - // jQuery's `is()` doesn't play well with HTML5 in IE - return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time"); - } - }); - - // functions that can be called via $(el).timeago('action') - // init is default when no action is given - // functions are called with context of a single element - var functions = { - init: function(){ - var refresh_el = $.proxy(refresh, this); - refresh_el(); - var $s = $t.settings; - if ($s.refreshMillis > 0) { - setInterval(refresh_el, $s.refreshMillis); - } - }, - update: function(time){ - $(this).data('timeago', { datetime: $t.parse(time) }); - refresh.apply(this); - } - }; - - $.fn.timeago = function(action, options) { - var fn = action ? functions[action] : functions.init; - if(!fn){ - throw new Error("Unknown function name '"+ action +"' for timeago"); - } - // each over objects here and call the requested function - this.each(function(){ - fn.call(this, options); - }); - return this; - }; - - function refresh() { - var data = prepareData(this); - if (!isNaN(data.datetime)) { - $(this).text(inWords(data.datetime)); - } - return this; - } - - function prepareData(element) { - element = $(element); - if (!element.data("timeago")) { - element.data("timeago", { datetime: $t.datetime(element) }); - var text = $.trim(element.text()); - if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) { - element.attr("title", text); - } - } - return element.data("timeago"); - } - - function inWords(date) { - return $t.inWords(distance(date)); - } - - function distance(date) { - return (new Date().getTime() - date.getTime()); - } - - // fix for IE6 suckage - document.createElement("abbr"); - document.createElement("time"); -})); diff --git a/app/assets/javascripts/lib/md5.js b/app/assets/javascripts/lib/md5.js deleted file mode 100644 index b63716eaad2..00000000000 --- a/app/assets/javascripts/lib/md5.js +++ /dev/null @@ -1,211 +0,0 @@ -function md5 (str) { - // http://kevin.vanzonneveld.net - // + original by: Webtoolkit.info (http://www.webtoolkit.info/) - // + namespaced by: Michael White (http://getsprink.com) - // + tweaked by: Jack - // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // + input by: Brett Zamir (http://brett-zamir.me) - // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // - depends on: utf8_encode - // * example 1: md5('Kevin van Zonneveld'); - // * returns 1: '6e658d4bfcb59cc13f96c14450ac40b9' - var xl; - - var rotateLeft = function (lValue, iShiftBits) { - return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits)); - }; - - var addUnsigned = function (lX, lY) { - var lX4, lY4, lX8, lY8, lResult; - lX8 = (lX & 0x80000000); - lY8 = (lY & 0x80000000); - lX4 = (lX & 0x40000000); - lY4 = (lY & 0x40000000); - lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF); - if (lX4 & lY4) { - return (lResult ^ 0x80000000 ^ lX8 ^ lY8); - } - if (lX4 | lY4) { - if (lResult & 0x40000000) { - return (lResult ^ 0xC0000000 ^ lX8 ^ lY8); - } else { - return (lResult ^ 0x40000000 ^ lX8 ^ lY8); - } - } else { - return (lResult ^ lX8 ^ lY8); - } - }; - - var _F = function (x, y, z) { - return (x & y) | ((~x) & z); - }; - var _G = function (x, y, z) { - return (x & z) | (y & (~z)); - }; - var _H = function (x, y, z) { - return (x ^ y ^ z); - }; - var _I = function (x, y, z) { - return (y ^ (x | (~z))); - }; - - var _FF = function (a, b, c, d, x, s, ac) { - a = addUnsigned(a, addUnsigned(addUnsigned(_F(b, c, d), x), ac)); - return addUnsigned(rotateLeft(a, s), b); - }; - - var _GG = function (a, b, c, d, x, s, ac) { - a = addUnsigned(a, addUnsigned(addUnsigned(_G(b, c, d), x), ac)); - return addUnsigned(rotateLeft(a, s), b); - }; - - var _HH = function (a, b, c, d, x, s, ac) { - a = addUnsigned(a, addUnsigned(addUnsigned(_H(b, c, d), x), ac)); - return addUnsigned(rotateLeft(a, s), b); - }; - - var _II = function (a, b, c, d, x, s, ac) { - a = addUnsigned(a, addUnsigned(addUnsigned(_I(b, c, d), x), ac)); - return addUnsigned(rotateLeft(a, s), b); - }; - - var convertToWordArray = function (str) { - var lWordCount; - var lMessageLength = str.length; - var lNumberOfWords_temp1 = lMessageLength + 8; - var lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64; - var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16; - var lWordArray = new Array(lNumberOfWords - 1); - var lBytePosition = 0; - var lByteCount = 0; - while (lByteCount < lMessageLength) { - lWordCount = (lByteCount - (lByteCount % 4)) / 4; - lBytePosition = (lByteCount % 4) * 8; - lWordArray[lWordCount] = (lWordArray[lWordCount] | (str.charCodeAt(lByteCount) << lBytePosition)); - lByteCount++; - } - lWordCount = (lByteCount - (lByteCount % 4)) / 4; - lBytePosition = (lByteCount % 4) * 8; - lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition); - lWordArray[lNumberOfWords - 2] = lMessageLength << 3; - lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29; - return lWordArray; - }; - - var wordToHex = function (lValue) { - var wordToHexValue = "", - wordToHexValue_temp = "", - lByte, lCount; - for (lCount = 0; lCount <= 3; lCount++) { - lByte = (lValue >>> (lCount * 8)) & 255; - wordToHexValue_temp = "0" + lByte.toString(16); - wordToHexValue = wordToHexValue + wordToHexValue_temp.substr(wordToHexValue_temp.length - 2, 2); - } - return wordToHexValue; - }; - - var x = [], - k, AA, BB, CC, DD, a, b, c, d, S11 = 7, - S12 = 12, - S13 = 17, - S14 = 22, - S21 = 5, - S22 = 9, - S23 = 14, - S24 = 20, - S31 = 4, - S32 = 11, - S33 = 16, - S34 = 23, - S41 = 6, - S42 = 10, - S43 = 15, - S44 = 21; - - str = this.utf8_encode(str); - x = convertToWordArray(str); - a = 0x67452301; - b = 0xEFCDAB89; - c = 0x98BADCFE; - d = 0x10325476; - - xl = x.length; - for (k = 0; k < xl; k += 16) { - AA = a; - BB = b; - CC = c; - DD = d; - a = _FF(a, b, c, d, x[k + 0], S11, 0xD76AA478); - d = _FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756); - c = _FF(c, d, a, b, x[k + 2], S13, 0x242070DB); - b = _FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE); - a = _FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF); - d = _FF(d, a, b, c, x[k + 5], S12, 0x4787C62A); - c = _FF(c, d, a, b, x[k + 6], S13, 0xA8304613); - b = _FF(b, c, d, a, x[k + 7], S14, 0xFD469501); - a = _FF(a, b, c, d, x[k + 8], S11, 0x698098D8); - d = _FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF); - c = _FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1); - b = _FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE); - a = _FF(a, b, c, d, x[k + 12], S11, 0x6B901122); - d = _FF(d, a, b, c, x[k + 13], S12, 0xFD987193); - c = _FF(c, d, a, b, x[k + 14], S13, 0xA679438E); - b = _FF(b, c, d, a, x[k + 15], S14, 0x49B40821); - a = _GG(a, b, c, d, x[k + 1], S21, 0xF61E2562); - d = _GG(d, a, b, c, x[k + 6], S22, 0xC040B340); - c = _GG(c, d, a, b, x[k + 11], S23, 0x265E5A51); - b = _GG(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA); - a = _GG(a, b, c, d, x[k + 5], S21, 0xD62F105D); - d = _GG(d, a, b, c, x[k + 10], S22, 0x2441453); - c = _GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681); - b = _GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8); - a = _GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6); - d = _GG(d, a, b, c, x[k + 14], S22, 0xC33707D6); - c = _GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87); - b = _GG(b, c, d, a, x[k + 8], S24, 0x455A14ED); - a = _GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905); - d = _GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8); - c = _GG(c, d, a, b, x[k + 7], S23, 0x676F02D9); - b = _GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A); - a = _HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942); - d = _HH(d, a, b, c, x[k + 8], S32, 0x8771F681); - c = _HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122); - b = _HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C); - a = _HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44); - d = _HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9); - c = _HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60); - b = _HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70); - a = _HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6); - d = _HH(d, a, b, c, x[k + 0], S32, 0xEAA127FA); - c = _HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085); - b = _HH(b, c, d, a, x[k + 6], S34, 0x4881D05); - a = _HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039); - d = _HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5); - c = _HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8); - b = _HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665); - a = _II(a, b, c, d, x[k + 0], S41, 0xF4292244); - d = _II(d, a, b, c, x[k + 7], S42, 0x432AFF97); - c = _II(c, d, a, b, x[k + 14], S43, 0xAB9423A7); - b = _II(b, c, d, a, x[k + 5], S44, 0xFC93A039); - a = _II(a, b, c, d, x[k + 12], S41, 0x655B59C3); - d = _II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92); - c = _II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D); - b = _II(b, c, d, a, x[k + 1], S44, 0x85845DD1); - a = _II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F); - d = _II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0); - c = _II(c, d, a, b, x[k + 6], S43, 0xA3014314); - b = _II(b, c, d, a, x[k + 13], S44, 0x4E0811A1); - a = _II(a, b, c, d, x[k + 4], S41, 0xF7537E82); - d = _II(d, a, b, c, x[k + 11], S42, 0xBD3AF235); - c = _II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB); - b = _II(b, c, d, a, x[k + 9], S44, 0xEB86D391); - a = addUnsigned(a, AA); - b = addUnsigned(b, BB); - c = addUnsigned(c, CC); - d = addUnsigned(d, DD); - } - - var temp = wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d); - - return temp.toLowerCase(); -} diff --git a/app/assets/javascripts/lib/notify.js.coffee b/app/assets/javascripts/lib/notify.js.coffee deleted file mode 100644 index 9e28353ac34..00000000000 --- a/app/assets/javascripts/lib/notify.js.coffee +++ /dev/null @@ -1,35 +0,0 @@ -((w) -> - notificationGranted = (message, opts, onclick) -> - notification = new Notification(message, opts) - - # Hide the notification after X amount of seconds - setTimeout -> - notification.close() - , 8000 - - if onclick - notification.onclick = onclick - - notifyPermissions = -> - if 'Notification' of window - Notification.requestPermission() - - notifyMe = (message, body, icon, onclick) -> - opts = - body: body - icon: icon - # Let's check if the browser supports notifications - if !('Notification' of window) - # do nothing - else if Notification.permission == 'granted' - # If it's okay let's create a notification - notificationGranted message, opts, onclick - else if Notification.permission != 'denied' - Notification.requestPermission (permission) -> - # If the user accepts, let's create a notification - if permission == 'granted' - notificationGranted message, opts, onclick - - w.notify = notifyMe - w.notifyPermissions = notifyPermissions -) window diff --git a/app/assets/javascripts/lib/raphael.js.coffee b/app/assets/javascripts/lib/raphael.js.coffee new file mode 100644 index 00000000000..ab8e5979b87 --- /dev/null +++ b/app/assets/javascripts/lib/raphael.js.coffee @@ -0,0 +1,3 @@ +#= require raphael +#= require g.raphael +#= require g.bar diff --git a/app/assets/javascripts/lib/raphael/application.js.coffee b/app/assets/javascripts/lib/raphael/application.js.coffee deleted file mode 100644 index ab8e5979b87..00000000000 --- a/app/assets/javascripts/lib/raphael/application.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -#= require raphael -#= require g.raphael -#= require g.bar diff --git a/app/assets/javascripts/lib/text_utility.js.coffee b/app/assets/javascripts/lib/text_utility.js.coffee deleted file mode 100644 index bb2772dfed2..00000000000 --- a/app/assets/javascripts/lib/text_utility.js.coffee +++ /dev/null @@ -1,79 +0,0 @@ -((w) -> - w.gl ?= {} - w.gl.text ?= {} - - gl.text.randomString = -> Math.random().toString(36).substring(7) - - gl.text.replaceRange = (s, start, end, substitute) -> - s.substring(0, start) + substitute + s.substring(end); - - gl.text.selectedText = (text, textarea) -> - text.substring(textarea.selectionStart, textarea.selectionEnd) - - gl.text.insertText = (textArea, text, tag, selected, wrap) -> - selectedSplit = selected.split('\n') - startChar = if not wrap and textArea.selectionStart > 0 then '\n' else '' - - if selectedSplit.length > 1 and not wrap - insertText = selectedSplit.map((val) -> - if val.indexOf(tag) is 0 - "#{val.replace(tag, '')}" - else - "#{tag}#{val}" - ).join('\n') - else - insertText = "#{startChar}#{tag}#{selected}#{if wrap then tag else ' '}" - - if document.queryCommandSupported('insertText') - document.execCommand 'insertText', false, insertText - else - try - document.execCommand("ms-beginUndoUnit") - - textArea.value = @replaceRange( - text, - textArea.selectionStart, - textArea.selectionEnd, - insertText) - try - document.execCommand("ms-endUndoUnit") - - @moveCursor(textArea, tag, wrap) - - gl.text.moveCursor = (textArea, tag, wrapped) -> - return unless textArea.setSelectionRange - - if textArea.selectionStart is textArea.selectionEnd - if wrapped - pos = textArea.selectionStart - tag.length - else - pos = textArea.selectionStart - - textArea.setSelectionRange pos, pos - - gl.text.updateText = (textArea, tag, wrap) -> - $textArea = $(textArea) - oldVal = $textArea.val() - textArea = $textArea.get(0) - text = $textArea.val() - selected = @selectedText(text, textArea) - $textArea.focus() - - @insertText(textArea, text, tag, selected, wrap) - - gl.text.init = (form) -> - self = @ - $('.js-md', form) - .off 'click' - .on 'click', -> - $this = $(@) - self.updateText( - $this.closest('.md-area').find('textarea'), - $this.data('md-tag'), - not $this.data('md-prepend') - ) - - gl.text.removeListeners = (form) -> - $('.js-md', form).off() - -) window diff --git a/app/assets/javascripts/lib/type_utility.js.coffee b/app/assets/javascripts/lib/type_utility.js.coffee deleted file mode 100644 index 957f0d86b36..00000000000 --- a/app/assets/javascripts/lib/type_utility.js.coffee +++ /dev/null @@ -1,9 +0,0 @@ -((w) -> - - w.gl ?= {} - w.gl.utils ?= {} - - w.gl.utils.isObject = (obj) -> - obj? and (obj.constructor is Object) - -) window diff --git a/app/assets/javascripts/lib/url_utility.js.coffee b/app/assets/javascripts/lib/url_utility.js.coffee deleted file mode 100644 index e8085e1c2e4..00000000000 --- a/app/assets/javascripts/lib/url_utility.js.coffee +++ /dev/null @@ -1,52 +0,0 @@ -((w) -> - - w.gl ?= {} - w.gl.utils ?= {} - - # Returns an array containing the value(s) of the - # of the key passed as an argument - w.gl.utils.getParameterValues = (sParam) -> - sPageURL = decodeURIComponent(window.location.search.substring(1)) - sURLVariables = sPageURL.split('&') - sParameterName = undefined - values = [] - i = 0 - while i < sURLVariables.length - sParameterName = sURLVariables[i].split('=') - if sParameterName[0] is sParam - values.push(sParameterName[1]) - i++ - values - - # # - # @param {Object} params - url keys and value to merge - # @param {String} url - # # - w.gl.utils.mergeUrlParams = (params, url) -> - newUrl = decodeURIComponent(url) - for paramName, paramValue of params - pattern = new RegExp "\\b(#{paramName}=).*?(&|$)" - if not paramValue? - newUrl = newUrl.replace pattern, '' - else if url.search(pattern) isnt -1 - newUrl = newUrl.replace pattern, "$1#{paramValue}$2" - else - newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}" - - # Remove a trailing ampersand - lastChar = newUrl[newUrl.length - 1] - - if lastChar is '&' - newUrl = newUrl.slice 0, -1 - - newUrl - - # removes parameter query string from url. returns the modified url - w.gl.utils.removeParamQueryString = (url, param) -> - url = decodeURIComponent(url) - urlVariables = url.split('&') - ( - variables for variables in urlVariables when variables.indexOf(param) is -1 - ).join('&') - -) window diff --git a/app/assets/javascripts/lib/utf8_encode.js b/app/assets/javascripts/lib/utf8_encode.js deleted file mode 100644 index 39ffe44dae0..00000000000 --- a/app/assets/javascripts/lib/utf8_encode.js +++ /dev/null @@ -1,70 +0,0 @@ -function utf8_encode (argString) { - // http://kevin.vanzonneveld.net - // + original by: Webtoolkit.info (http://www.webtoolkit.info/) - // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // + improved by: sowberry - // + tweaked by: Jack - // + bugfixed by: Onno Marsman - // + improved by: Yves Sucaet - // + bugfixed by: Onno Marsman - // + bugfixed by: Ulrich - // + bugfixed by: Rafal Kukawski - // + improved by: kirilloid - // + bugfixed by: kirilloid - // * example 1: utf8_encode('Kevin van Zonneveld'); - // * returns 1: 'Kevin van Zonneveld' - - if (argString === null || typeof argString === "undefined") { - return ""; - } - - var string = (argString + ''); // .replace(/\r\n/g, "\n").replace(/\r/g, "\n"); - var utftext = '', - start, end, stringl = 0; - - start = end = 0; - stringl = string.length; - for (var n = 0; n < stringl; n++) { - var c1 = string.charCodeAt(n); - var enc = null; - - if (c1 < 128) { - end++; - } else if (c1 > 127 && c1 < 2048) { - enc = String.fromCharCode( - (c1 >> 6) | 192, - ( c1 & 63) | 128 - ); - } else if (c1 & 0xF800 != 0xD800) { - enc = String.fromCharCode( - (c1 >> 12) | 224, - ((c1 >> 6) & 63) | 128, - ( c1 & 63) | 128 - ); - } else { // surrogate pairs - if (c1 & 0xFC00 != 0xD800) { throw new RangeError("Unmatched trail surrogate at " + n); } - var c2 = string.charCodeAt(++n); - if (c2 & 0xFC00 != 0xDC00) { throw new RangeError("Unmatched lead surrogate at " + (n-1)); } - c1 = ((c1 & 0x3FF) << 10) + (c2 & 0x3FF) + 0x10000; - enc = String.fromCharCode( - (c1 >> 18) | 240, - ((c1 >> 12) & 63) | 128, - ((c1 >> 6) & 63) | 128, - ( c1 & 63) | 128 - ); - } - if (enc !== null) { - if (end > start) { - utftext += string.slice(start, end); - } - utftext += enc; - start = end = n + 1; - } - } - - if (end > start) { - utftext += string.slice(start, stringl); - } - - return utftext; -} diff --git a/app/assets/javascripts/lib/utils/animate.js.coffee b/app/assets/javascripts/lib/utils/animate.js.coffee new file mode 100644 index 00000000000..ec3b44d6126 --- /dev/null +++ b/app/assets/javascripts/lib/utils/animate.js.coffee @@ -0,0 +1,39 @@ +((w) -> + if not w.gl? then w.gl = {} + if not gl.animate? then gl.animate = {} + + gl.animate.animate = ($el, animation, options, done) -> + if options?.cssStart? + $el.css(options.cssStart) + $el + .removeClass(animation + ' animated') + .addClass(animation + ' animated') + .one 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', -> + $(this).removeClass(animation + ' animated') + if done? + done() + if options?.cssEnd? + $el.css(options.cssEnd) + return + return + + gl.animate.animateEach = ($els, animation, time, options, done) -> + dfd = $.Deferred() + if not $els.length + dfd.resolve() + $els.each((i) -> + setTimeout(=> + $this = $(@) + gl.animate.animate($this, animation, options, => + if i is $els.length - 1 + dfd.resolve() + if done? + done() + ) + ,time * i + ) + return + ) + return dfd.promise() + return +) window \ No newline at end of file diff --git a/app/assets/javascripts/lib/utils/common_utils.js.coffee b/app/assets/javascripts/lib/utils/common_utils.js.coffee new file mode 100644 index 00000000000..e39dcb2daa9 --- /dev/null +++ b/app/assets/javascripts/lib/utils/common_utils.js.coffee @@ -0,0 +1,65 @@ +((w) -> + + w.gl or= {} + w.gl.utils or= {} + + w.gl.utils.isInGroupsPage = -> + + return $('body').data('page').split(':')[0] is 'groups' + + + w.gl.utils.isInProjectPage = -> + + return $('body').data('page').split(':')[0] is 'projects' + + + w.gl.utils.getProjectSlug = -> + + return if @isInProjectPage() then $('body').data 'project' else null + + + w.gl.utils.getGroupSlug = -> + + return if @isInGroupsPage() then $('body').data 'group' else null + + + + gl.utils.updateTooltipTitle = ($tooltipEl, newTitle) -> + + $tooltipEl + .tooltip 'destroy' + .attr 'title', newTitle + .tooltip 'fixTitle' + + + gl.utils.preventDisabledButtons = -> + + $('.btn').click (e) -> + if $(this).hasClass 'disabled' + e.preventDefault() + e.stopImmediatePropagation() + return false + + + jQuery.timefor = (time, suffix, expiredLabel) -> + + return '' unless time + + suffix or= 'remaining' + expiredLabel or= 'Past due' + + jQuery.timeago.settings.allowFuture = yes + + { suffixFromNow } = jQuery.timeago.settings.strings + jQuery.timeago.settings.strings.suffixFromNow = suffix + + timefor = $.timeago time + + if timefor.indexOf('ago') > -1 + timefor = expiredLabel + + jQuery.timeago.settings.strings.suffixFromNow = suffixFromNow + + return timefor + +) window diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js.coffee b/app/assets/javascripts/lib/utils/datetime_utility.js.coffee new file mode 100644 index 00000000000..948d6dbf07e --- /dev/null +++ b/app/assets/javascripts/lib/utils/datetime_utility.js.coffee @@ -0,0 +1,24 @@ +((w) -> + + w.gl ?= {} + w.gl.utils ?= {} + + w.gl.utils.formatDate = (datetime) -> + dateFormat(datetime, 'mmm d, yyyy h:MMtt Z') + + w.gl.utils.localTimeAgo = ($timeagoEls, setTimeago = true) -> + $timeagoEls.each( -> + $el = $(@) + $el.attr('title', gl.utils.formatDate($el.attr('datetime'))) + ) + + if setTimeago + $timeagoEls.timeago() + $timeagoEls.tooltip('destroy') + + # Recreate with custom template + $timeagoEls.tooltip( + template: '' + ) + +) window diff --git a/app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb b/app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb new file mode 100644 index 00000000000..80f9936b9c2 --- /dev/null +++ b/app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb @@ -0,0 +1,2 @@ +gl.emojiAliases = -> + JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>') diff --git a/app/assets/javascripts/lib/utils/jquery.timeago.js b/app/assets/javascripts/lib/utils/jquery.timeago.js new file mode 100644 index 00000000000..cc17aa7d3d1 --- /dev/null +++ b/app/assets/javascripts/lib/utils/jquery.timeago.js @@ -0,0 +1,181 @@ +/** + * Timeago is a jQuery plugin that makes it easy to support automatically + * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago"). + * + * @name timeago + * @version 1.1.0 + * @requires jQuery v1.2.3+ + * @author Ryan McGeary + * @license MIT License - http://www.opensource.org/licenses/mit-license.php + * + * For usage and examples, visit: + * http://timeago.yarp.com/ + * + * Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org) + */ + +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else { + // Browser globals + factory(jQuery); + } +}(function ($) { + $.timeago = function(timestamp) { + if (timestamp instanceof Date) { + return inWords(timestamp); + } else if (typeof timestamp === "string") { + return inWords($.timeago.parse(timestamp)); + } else if (typeof timestamp === "number") { + return inWords(new Date(timestamp)); + } else { + return inWords($.timeago.datetime(timestamp)); + } + }; + var $t = $.timeago; + + $.extend($.timeago, { + settings: { + refreshMillis: 60000, + allowFuture: false, + strings: { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "ago", + suffixFromNow: "from now", + seconds: "less than a minute", + minute: "about a minute", + minutes: "%d minutes", + hour: "about an hour", + hours: "about %d hours", + day: "a day", + days: "%d days", + month: "about a month", + months: "%d months", + year: "about a year", + years: "%d years", + wordSeparator: " ", + numbers: [] + } + }, + inWords: function(distanceMillis) { + var $l = this.settings.strings; + var prefix = $l.prefixAgo; + var suffix = $l.suffixAgo; + if (this.settings.allowFuture) { + if (distanceMillis < 0) { + prefix = $l.prefixFromNow; + suffix = $l.suffixFromNow; + } + } + + var seconds = Math.abs(distanceMillis) / 1000; + var minutes = seconds / 60; + var hours = minutes / 60; + var days = hours / 24; + var years = days / 365; + + function substitute(stringOrFunction, number) { + var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction; + var value = ($l.numbers && $l.numbers[number]) || number; + return string.replace(/%d/i, value); + } + + var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) || + seconds < 90 && substitute($l.minute, 1) || + minutes < 45 && substitute($l.minutes, Math.round(minutes)) || + minutes < 90 && substitute($l.hour, 1) || + hours < 24 && substitute($l.hours, Math.round(hours)) || + hours < 42 && substitute($l.day, 1) || + days < 30 && substitute($l.days, Math.round(days)) || + days < 45 && substitute($l.month, 1) || + days < 365 && substitute($l.months, Math.round(days / 30)) || + years < 1.5 && substitute($l.year, 1) || + substitute($l.years, Math.round(years)); + + var separator = $l.wordSeparator || ""; + if ($l.wordSeparator === undefined) { separator = " "; } + return $.trim([prefix, words, suffix].join(separator)); + }, + parse: function(iso8601) { + var s = $.trim(iso8601); + s = s.replace(/\.\d+/,""); // remove milliseconds + s = s.replace(/-/,"/").replace(/-/,"/"); + s = s.replace(/T/," ").replace(/Z/," UTC"); + s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400 + return new Date(s); + }, + datetime: function(elem) { + var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title"); + return $t.parse(iso8601); + }, + isTime: function(elem) { + // jQuery's `is()` doesn't play well with HTML5 in IE + return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time"); + } + }); + + // functions that can be called via $(el).timeago('action') + // init is default when no action is given + // functions are called with context of a single element + var functions = { + init: function(){ + var refresh_el = $.proxy(refresh, this); + refresh_el(); + var $s = $t.settings; + if ($s.refreshMillis > 0) { + setInterval(refresh_el, $s.refreshMillis); + } + }, + update: function(time){ + $(this).data('timeago', { datetime: $t.parse(time) }); + refresh.apply(this); + } + }; + + $.fn.timeago = function(action, options) { + var fn = action ? functions[action] : functions.init; + if(!fn){ + throw new Error("Unknown function name '"+ action +"' for timeago"); + } + // each over objects here and call the requested function + this.each(function(){ + fn.call(this, options); + }); + return this; + }; + + function refresh() { + var data = prepareData(this); + if (!isNaN(data.datetime)) { + $(this).text(inWords(data.datetime)); + } + return this; + } + + function prepareData(element) { + element = $(element); + if (!element.data("timeago")) { + element.data("timeago", { datetime: $t.datetime(element) }); + var text = $.trim(element.text()); + if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) { + element.attr("title", text); + } + } + return element.data("timeago"); + } + + function inWords(date) { + return $t.inWords(distance(date)); + } + + function distance(date) { + return (new Date().getTime() - date.getTime()); + } + + // fix for IE6 suckage + document.createElement("abbr"); + document.createElement("time"); +})); diff --git a/app/assets/javascripts/lib/utils/md5.js b/app/assets/javascripts/lib/utils/md5.js new file mode 100644 index 00000000000..b63716eaad2 --- /dev/null +++ b/app/assets/javascripts/lib/utils/md5.js @@ -0,0 +1,211 @@ +function md5 (str) { + // http://kevin.vanzonneveld.net + // + original by: Webtoolkit.info (http://www.webtoolkit.info/) + // + namespaced by: Michael White (http://getsprink.com) + // + tweaked by: Jack + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + input by: Brett Zamir (http://brett-zamir.me) + // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // - depends on: utf8_encode + // * example 1: md5('Kevin van Zonneveld'); + // * returns 1: '6e658d4bfcb59cc13f96c14450ac40b9' + var xl; + + var rotateLeft = function (lValue, iShiftBits) { + return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits)); + }; + + var addUnsigned = function (lX, lY) { + var lX4, lY4, lX8, lY8, lResult; + lX8 = (lX & 0x80000000); + lY8 = (lY & 0x80000000); + lX4 = (lX & 0x40000000); + lY4 = (lY & 0x40000000); + lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF); + if (lX4 & lY4) { + return (lResult ^ 0x80000000 ^ lX8 ^ lY8); + } + if (lX4 | lY4) { + if (lResult & 0x40000000) { + return (lResult ^ 0xC0000000 ^ lX8 ^ lY8); + } else { + return (lResult ^ 0x40000000 ^ lX8 ^ lY8); + } + } else { + return (lResult ^ lX8 ^ lY8); + } + }; + + var _F = function (x, y, z) { + return (x & y) | ((~x) & z); + }; + var _G = function (x, y, z) { + return (x & z) | (y & (~z)); + }; + var _H = function (x, y, z) { + return (x ^ y ^ z); + }; + var _I = function (x, y, z) { + return (y ^ (x | (~z))); + }; + + var _FF = function (a, b, c, d, x, s, ac) { + a = addUnsigned(a, addUnsigned(addUnsigned(_F(b, c, d), x), ac)); + return addUnsigned(rotateLeft(a, s), b); + }; + + var _GG = function (a, b, c, d, x, s, ac) { + a = addUnsigned(a, addUnsigned(addUnsigned(_G(b, c, d), x), ac)); + return addUnsigned(rotateLeft(a, s), b); + }; + + var _HH = function (a, b, c, d, x, s, ac) { + a = addUnsigned(a, addUnsigned(addUnsigned(_H(b, c, d), x), ac)); + return addUnsigned(rotateLeft(a, s), b); + }; + + var _II = function (a, b, c, d, x, s, ac) { + a = addUnsigned(a, addUnsigned(addUnsigned(_I(b, c, d), x), ac)); + return addUnsigned(rotateLeft(a, s), b); + }; + + var convertToWordArray = function (str) { + var lWordCount; + var lMessageLength = str.length; + var lNumberOfWords_temp1 = lMessageLength + 8; + var lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64; + var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16; + var lWordArray = new Array(lNumberOfWords - 1); + var lBytePosition = 0; + var lByteCount = 0; + while (lByteCount < lMessageLength) { + lWordCount = (lByteCount - (lByteCount % 4)) / 4; + lBytePosition = (lByteCount % 4) * 8; + lWordArray[lWordCount] = (lWordArray[lWordCount] | (str.charCodeAt(lByteCount) << lBytePosition)); + lByteCount++; + } + lWordCount = (lByteCount - (lByteCount % 4)) / 4; + lBytePosition = (lByteCount % 4) * 8; + lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition); + lWordArray[lNumberOfWords - 2] = lMessageLength << 3; + lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29; + return lWordArray; + }; + + var wordToHex = function (lValue) { + var wordToHexValue = "", + wordToHexValue_temp = "", + lByte, lCount; + for (lCount = 0; lCount <= 3; lCount++) { + lByte = (lValue >>> (lCount * 8)) & 255; + wordToHexValue_temp = "0" + lByte.toString(16); + wordToHexValue = wordToHexValue + wordToHexValue_temp.substr(wordToHexValue_temp.length - 2, 2); + } + return wordToHexValue; + }; + + var x = [], + k, AA, BB, CC, DD, a, b, c, d, S11 = 7, + S12 = 12, + S13 = 17, + S14 = 22, + S21 = 5, + S22 = 9, + S23 = 14, + S24 = 20, + S31 = 4, + S32 = 11, + S33 = 16, + S34 = 23, + S41 = 6, + S42 = 10, + S43 = 15, + S44 = 21; + + str = this.utf8_encode(str); + x = convertToWordArray(str); + a = 0x67452301; + b = 0xEFCDAB89; + c = 0x98BADCFE; + d = 0x10325476; + + xl = x.length; + for (k = 0; k < xl; k += 16) { + AA = a; + BB = b; + CC = c; + DD = d; + a = _FF(a, b, c, d, x[k + 0], S11, 0xD76AA478); + d = _FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756); + c = _FF(c, d, a, b, x[k + 2], S13, 0x242070DB); + b = _FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE); + a = _FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF); + d = _FF(d, a, b, c, x[k + 5], S12, 0x4787C62A); + c = _FF(c, d, a, b, x[k + 6], S13, 0xA8304613); + b = _FF(b, c, d, a, x[k + 7], S14, 0xFD469501); + a = _FF(a, b, c, d, x[k + 8], S11, 0x698098D8); + d = _FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF); + c = _FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1); + b = _FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE); + a = _FF(a, b, c, d, x[k + 12], S11, 0x6B901122); + d = _FF(d, a, b, c, x[k + 13], S12, 0xFD987193); + c = _FF(c, d, a, b, x[k + 14], S13, 0xA679438E); + b = _FF(b, c, d, a, x[k + 15], S14, 0x49B40821); + a = _GG(a, b, c, d, x[k + 1], S21, 0xF61E2562); + d = _GG(d, a, b, c, x[k + 6], S22, 0xC040B340); + c = _GG(c, d, a, b, x[k + 11], S23, 0x265E5A51); + b = _GG(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA); + a = _GG(a, b, c, d, x[k + 5], S21, 0xD62F105D); + d = _GG(d, a, b, c, x[k + 10], S22, 0x2441453); + c = _GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681); + b = _GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8); + a = _GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6); + d = _GG(d, a, b, c, x[k + 14], S22, 0xC33707D6); + c = _GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87); + b = _GG(b, c, d, a, x[k + 8], S24, 0x455A14ED); + a = _GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905); + d = _GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8); + c = _GG(c, d, a, b, x[k + 7], S23, 0x676F02D9); + b = _GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A); + a = _HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942); + d = _HH(d, a, b, c, x[k + 8], S32, 0x8771F681); + c = _HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122); + b = _HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C); + a = _HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44); + d = _HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9); + c = _HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60); + b = _HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70); + a = _HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6); + d = _HH(d, a, b, c, x[k + 0], S32, 0xEAA127FA); + c = _HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085); + b = _HH(b, c, d, a, x[k + 6], S34, 0x4881D05); + a = _HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039); + d = _HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5); + c = _HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8); + b = _HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665); + a = _II(a, b, c, d, x[k + 0], S41, 0xF4292244); + d = _II(d, a, b, c, x[k + 7], S42, 0x432AFF97); + c = _II(c, d, a, b, x[k + 14], S43, 0xAB9423A7); + b = _II(b, c, d, a, x[k + 5], S44, 0xFC93A039); + a = _II(a, b, c, d, x[k + 12], S41, 0x655B59C3); + d = _II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92); + c = _II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D); + b = _II(b, c, d, a, x[k + 1], S44, 0x85845DD1); + a = _II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F); + d = _II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0); + c = _II(c, d, a, b, x[k + 6], S43, 0xA3014314); + b = _II(b, c, d, a, x[k + 13], S44, 0x4E0811A1); + a = _II(a, b, c, d, x[k + 4], S41, 0xF7537E82); + d = _II(d, a, b, c, x[k + 11], S42, 0xBD3AF235); + c = _II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB); + b = _II(b, c, d, a, x[k + 9], S44, 0xEB86D391); + a = addUnsigned(a, AA); + b = addUnsigned(b, BB); + c = addUnsigned(c, CC); + d = addUnsigned(d, DD); + } + + var temp = wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d); + + return temp.toLowerCase(); +} diff --git a/app/assets/javascripts/lib/utils/notify.js.coffee b/app/assets/javascripts/lib/utils/notify.js.coffee new file mode 100644 index 00000000000..9e28353ac34 --- /dev/null +++ b/app/assets/javascripts/lib/utils/notify.js.coffee @@ -0,0 +1,35 @@ +((w) -> + notificationGranted = (message, opts, onclick) -> + notification = new Notification(message, opts) + + # Hide the notification after X amount of seconds + setTimeout -> + notification.close() + , 8000 + + if onclick + notification.onclick = onclick + + notifyPermissions = -> + if 'Notification' of window + Notification.requestPermission() + + notifyMe = (message, body, icon, onclick) -> + opts = + body: body + icon: icon + # Let's check if the browser supports notifications + if !('Notification' of window) + # do nothing + else if Notification.permission == 'granted' + # If it's okay let's create a notification + notificationGranted message, opts, onclick + else if Notification.permission != 'denied' + Notification.requestPermission (permission) -> + # If the user accepts, let's create a notification + if permission == 'granted' + notificationGranted message, opts, onclick + + w.notify = notifyMe + w.notifyPermissions = notifyPermissions +) window diff --git a/app/assets/javascripts/lib/utils/text_utility.js.coffee b/app/assets/javascripts/lib/utils/text_utility.js.coffee new file mode 100644 index 00000000000..bb2772dfed2 --- /dev/null +++ b/app/assets/javascripts/lib/utils/text_utility.js.coffee @@ -0,0 +1,79 @@ +((w) -> + w.gl ?= {} + w.gl.text ?= {} + + gl.text.randomString = -> Math.random().toString(36).substring(7) + + gl.text.replaceRange = (s, start, end, substitute) -> + s.substring(0, start) + substitute + s.substring(end); + + gl.text.selectedText = (text, textarea) -> + text.substring(textarea.selectionStart, textarea.selectionEnd) + + gl.text.insertText = (textArea, text, tag, selected, wrap) -> + selectedSplit = selected.split('\n') + startChar = if not wrap and textArea.selectionStart > 0 then '\n' else '' + + if selectedSplit.length > 1 and not wrap + insertText = selectedSplit.map((val) -> + if val.indexOf(tag) is 0 + "#{val.replace(tag, '')}" + else + "#{tag}#{val}" + ).join('\n') + else + insertText = "#{startChar}#{tag}#{selected}#{if wrap then tag else ' '}" + + if document.queryCommandSupported('insertText') + document.execCommand 'insertText', false, insertText + else + try + document.execCommand("ms-beginUndoUnit") + + textArea.value = @replaceRange( + text, + textArea.selectionStart, + textArea.selectionEnd, + insertText) + try + document.execCommand("ms-endUndoUnit") + + @moveCursor(textArea, tag, wrap) + + gl.text.moveCursor = (textArea, tag, wrapped) -> + return unless textArea.setSelectionRange + + if textArea.selectionStart is textArea.selectionEnd + if wrapped + pos = textArea.selectionStart - tag.length + else + pos = textArea.selectionStart + + textArea.setSelectionRange pos, pos + + gl.text.updateText = (textArea, tag, wrap) -> + $textArea = $(textArea) + oldVal = $textArea.val() + textArea = $textArea.get(0) + text = $textArea.val() + selected = @selectedText(text, textArea) + $textArea.focus() + + @insertText(textArea, text, tag, selected, wrap) + + gl.text.init = (form) -> + self = @ + $('.js-md', form) + .off 'click' + .on 'click', -> + $this = $(@) + self.updateText( + $this.closest('.md-area').find('textarea'), + $this.data('md-tag'), + not $this.data('md-prepend') + ) + + gl.text.removeListeners = (form) -> + $('.js-md', form).off() + +) window diff --git a/app/assets/javascripts/lib/utils/type_utility.js.coffee b/app/assets/javascripts/lib/utils/type_utility.js.coffee new file mode 100644 index 00000000000..957f0d86b36 --- /dev/null +++ b/app/assets/javascripts/lib/utils/type_utility.js.coffee @@ -0,0 +1,9 @@ +((w) -> + + w.gl ?= {} + w.gl.utils ?= {} + + w.gl.utils.isObject = (obj) -> + obj? and (obj.constructor is Object) + +) window diff --git a/app/assets/javascripts/lib/utils/url_utility.js.coffee b/app/assets/javascripts/lib/utils/url_utility.js.coffee new file mode 100644 index 00000000000..e8085e1c2e4 --- /dev/null +++ b/app/assets/javascripts/lib/utils/url_utility.js.coffee @@ -0,0 +1,52 @@ +((w) -> + + w.gl ?= {} + w.gl.utils ?= {} + + # Returns an array containing the value(s) of the + # of the key passed as an argument + w.gl.utils.getParameterValues = (sParam) -> + sPageURL = decodeURIComponent(window.location.search.substring(1)) + sURLVariables = sPageURL.split('&') + sParameterName = undefined + values = [] + i = 0 + while i < sURLVariables.length + sParameterName = sURLVariables[i].split('=') + if sParameterName[0] is sParam + values.push(sParameterName[1]) + i++ + values + + # # + # @param {Object} params - url keys and value to merge + # @param {String} url + # # + w.gl.utils.mergeUrlParams = (params, url) -> + newUrl = decodeURIComponent(url) + for paramName, paramValue of params + pattern = new RegExp "\\b(#{paramName}=).*?(&|$)" + if not paramValue? + newUrl = newUrl.replace pattern, '' + else if url.search(pattern) isnt -1 + newUrl = newUrl.replace pattern, "$1#{paramValue}$2" + else + newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}" + + # Remove a trailing ampersand + lastChar = newUrl[newUrl.length - 1] + + if lastChar is '&' + newUrl = newUrl.slice 0, -1 + + newUrl + + # removes parameter query string from url. returns the modified url + w.gl.utils.removeParamQueryString = (url, param) -> + url = decodeURIComponent(url) + urlVariables = url.split('&') + ( + variables for variables in urlVariables when variables.indexOf(param) is -1 + ).join('&') + +) window diff --git a/app/assets/javascripts/lib/utils/utf8_encode.js b/app/assets/javascripts/lib/utils/utf8_encode.js new file mode 100644 index 00000000000..39ffe44dae0 --- /dev/null +++ b/app/assets/javascripts/lib/utils/utf8_encode.js @@ -0,0 +1,70 @@ +function utf8_encode (argString) { + // http://kevin.vanzonneveld.net + // + original by: Webtoolkit.info (http://www.webtoolkit.info/) + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + improved by: sowberry + // + tweaked by: Jack + // + bugfixed by: Onno Marsman + // + improved by: Yves Sucaet + // + bugfixed by: Onno Marsman + // + bugfixed by: Ulrich + // + bugfixed by: Rafal Kukawski + // + improved by: kirilloid + // + bugfixed by: kirilloid + // * example 1: utf8_encode('Kevin van Zonneveld'); + // * returns 1: 'Kevin van Zonneveld' + + if (argString === null || typeof argString === "undefined") { + return ""; + } + + var string = (argString + ''); // .replace(/\r\n/g, "\n").replace(/\r/g, "\n"); + var utftext = '', + start, end, stringl = 0; + + start = end = 0; + stringl = string.length; + for (var n = 0; n < stringl; n++) { + var c1 = string.charCodeAt(n); + var enc = null; + + if (c1 < 128) { + end++; + } else if (c1 > 127 && c1 < 2048) { + enc = String.fromCharCode( + (c1 >> 6) | 192, + ( c1 & 63) | 128 + ); + } else if (c1 & 0xF800 != 0xD800) { + enc = String.fromCharCode( + (c1 >> 12) | 224, + ((c1 >> 6) & 63) | 128, + ( c1 & 63) | 128 + ); + } else { // surrogate pairs + if (c1 & 0xFC00 != 0xD800) { throw new RangeError("Unmatched trail surrogate at " + n); } + var c2 = string.charCodeAt(++n); + if (c2 & 0xFC00 != 0xDC00) { throw new RangeError("Unmatched lead surrogate at " + (n-1)); } + c1 = ((c1 & 0x3FF) << 10) + (c2 & 0x3FF) + 0x10000; + enc = String.fromCharCode( + (c1 >> 18) | 240, + ((c1 >> 12) & 63) | 128, + ((c1 >> 6) & 63) | 128, + ( c1 & 63) | 128 + ); + } + if (enc !== null) { + if (end > start) { + utftext += string.slice(start, end); + } + utftext += enc; + start = end = n + 1; + } + } + + if (end > start) { + utftext += string.slice(start, stringl); + } + + return utftext; +} diff --git a/app/assets/javascripts/network/application.js.coffee b/app/assets/javascripts/network/application.js.coffee index cb9eead855b..f75f63869c5 100644 --- a/app/assets/javascripts/network/application.js.coffee +++ b/app/assets/javascripts/network/application.js.coffee @@ -4,9 +4,6 @@ # It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the # the compiled file. # -#= require raphael -#= require g.raphael -#= require g.bar #= require_tree . $ -> diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index 5ea4adcd0fa..ca04a782fcf 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -2,7 +2,7 @@ %ul{ class: (container_class) } - content_for :page_specific_javascripts do - = javascript_include_tag 'lib/chart/application.js', {integrity: true, "data-turbolinks-track" => true} + = javascript_include_tag 'lib/chart.js', {integrity: true, "data-turbolinks-track" => true} = javascript_include_tag 'graphs/application.js', {integrity: true, "data-turbolinks-track" => true} = nav_link(action: :show) do = link_to 'Contributors', namespace_project_graph_path diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index 749afe6b6ab..8bfda9776c7 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -1,6 +1,6 @@ - page_title "Network", @ref - content_for :page_specific_javascripts do - = javascript_include_tag 'lib/raphael/application.js', {integrity: true, "data-turbolinks-track" => true} + = javascript_include_tag 'lib/raphael.js', {integrity: true, "data-turbolinks-track" => true} = javascript_include_tag 'network/application.js', {integrity: true, "data-turbolinks-track" => true} = render "projects/commits/head" = render "head" diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 9ca351e5147..0fabc2a7714 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,7 +1,7 @@ - page_title @user.name - page_description @user.bio - content_for :page_specific_javascripts do - = javascript_include_tag 'lib/d3/application.js', {integrity: true, "data-turbolinks-track" => true} + = javascript_include_tag 'lib/d3.js', {integrity: true, "data-turbolinks-track" => true} = javascript_include_tag 'users/application.js', {integrity: true, "data-turbolinks-track" => true} - header_title @user.name, user_path(@user) - @no_container = true -- cgit v1.2.1 From 40db56a26c62286febdb0656b9cc862dca99cfae Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 23 Jun 2016 11:05:02 -0600 Subject: Add precompilation for relevant assets. --- config/application.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/application.rb b/config/application.rb index 05fec995ed3..2b0595ede2b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -84,6 +84,8 @@ module Gitlab config.assets.precompile << "graphs/application.js" config.assets.precompile << "users/application.js" config.assets.precompile << "network/application.js" + config.assets.precompile << "lib/utils/*.js" + config.assets.precompile << "lib/*.js" # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' -- cgit v1.2.1 From 5321f817854b2d2c6b96fe4e4f14560458940692 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 23 Jun 2016 11:09:46 -0600 Subject: Readability. --- app/views/layouts/_head.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index bc3118f70aa..2d020e9c222 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -30,7 +30,7 @@ = javascript_include_tag "application", integrity: true - - if :page_specific_javascripts + - if content_for?(:page_specific_javascripts) = yield :page_specific_javascripts = csrf_meta_tags -- cgit v1.2.1 From f82278c49cdda0db28c45143f6b19b3c4ca90f3e Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 23 Jun 2016 11:21:35 -0600 Subject: Fix teaspoon tests. --- spec/javascripts/application_spec.js.coffee | 2 +- spec/javascripts/issue_spec.js.coffee | 2 +- spec/javascripts/project_title_spec.js.coffee | 2 +- spec/javascripts/search_autocomplete_spec.js.coffee | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/javascripts/application_spec.js.coffee b/spec/javascripts/application_spec.js.coffee index 8af39c41f2f..4b6a2bb5440 100644 --- a/spec/javascripts/application_spec.js.coffee +++ b/spec/javascripts/application_spec.js.coffee @@ -1,4 +1,4 @@ -#= require lib/common_utils +#= require lib/utils/common_utils describe 'Application', -> describe 'disable buttons', -> diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee index 71f0c1076c5..d84d80f266b 100644 --- a/spec/javascripts/issue_spec.js.coffee +++ b/spec/javascripts/issue_spec.js.coffee @@ -1,4 +1,4 @@ -#= require lib/text_utility +#= require lib/utils/text_utility #= require issue describe 'Issue', -> diff --git a/spec/javascripts/project_title_spec.js.coffee b/spec/javascripts/project_title_spec.js.coffee index 9be29097f4c..f0d26fb5446 100644 --- a/spec/javascripts/project_title_spec.js.coffee +++ b/spec/javascripts/project_title_spec.js.coffee @@ -1,6 +1,6 @@ #= require bootstrap #= require select2 -#= require lib/type_utility +#= require lib/utils/type_utility #= require gl_dropdown #= require api #= require project_select diff --git a/spec/javascripts/search_autocomplete_spec.js.coffee b/spec/javascripts/search_autocomplete_spec.js.coffee index e77177783a7..1c1faca3333 100644 --- a/spec/javascripts/search_autocomplete_spec.js.coffee +++ b/spec/javascripts/search_autocomplete_spec.js.coffee @@ -1,8 +1,8 @@ #= require gl_dropdown #= require search_autocomplete #= require jquery -#= require lib/common_utils -#= require lib/type_utility +#= require lib/utils/common_utils +#= require lib/utils/type_utility #= require fuzzaldrin-plus -- cgit v1.2.1 From ff01ca9c96dc001fabf5e5a1200bc3355c61a3ca Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 23 Jun 2016 11:42:52 -0600 Subject: Trying to make this work with sprockets in production. --- app/helpers/javascript_helper.rb | 6 ++---- app/views/projects/graphs/_head.html.haml | 4 ++-- app/views/projects/network/show.html.haml | 4 ++-- app/views/users/show.html.haml | 4 ++-- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb index 91dd91718dc..4bdc8e516ac 100644 --- a/app/helpers/javascript_helper.rb +++ b/app/helpers/javascript_helper.rb @@ -1,7 +1,5 @@ module JavascriptHelper - def page_specific_javascripts(js = nil) - @page_specific_javascripts = js unless js.nil? - - @page_specific_javascripts + def page_specific_javascript_tag(js) + javascript_include_tag asset_path(js), {integrity: true, "data-turbolinks-track" => true} end end diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index ca04a782fcf..ca347406dfe 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -2,8 +2,8 @@ %ul{ class: (container_class) } - content_for :page_specific_javascripts do - = javascript_include_tag 'lib/chart.js', {integrity: true, "data-turbolinks-track" => true} - = javascript_include_tag 'graphs/application.js', {integrity: true, "data-turbolinks-track" => true} + = page_specific_javascript_tag('lib/chart.js') + = page_specific_javascript_tag('graphs/application.js') = nav_link(action: :show) do = link_to 'Contributors', namespace_project_graph_path = nav_link(action: :commits) do diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index 8bfda9776c7..3ca30b4ba6b 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -1,7 +1,7 @@ - page_title "Network", @ref - content_for :page_specific_javascripts do - = javascript_include_tag 'lib/raphael.js', {integrity: true, "data-turbolinks-track" => true} - = javascript_include_tag 'network/application.js', {integrity: true, "data-turbolinks-track" => true} + = page_specific_javascript_tag('lib/raphael.js') + = page_specific_javascript_tag('network/application.js') = render "projects/commits/head" = render "head" %div{ class: (container_class) } diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 0fabc2a7714..68665858c3e 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,8 +1,8 @@ - page_title @user.name - page_description @user.bio - content_for :page_specific_javascripts do - = javascript_include_tag 'lib/d3.js', {integrity: true, "data-turbolinks-track" => true} - = javascript_include_tag 'users/application.js', {integrity: true, "data-turbolinks-track" => true} + = page_specific_javascript_tag('lib/d3.js') + = page_specific_javascript_tag('users/application.js') - header_title @user.name, user_path(@user) - @no_container = true -- cgit v1.2.1 From 3e363a5a79dd0f03e7b0feb8c028f34e7ad45330 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 23 Jun 2016 11:58:20 -0600 Subject: Appease Rubocop. --- app/helpers/javascript_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb index 4bdc8e516ac..5109356941d 100644 --- a/app/helpers/javascript_helper.rb +++ b/app/helpers/javascript_helper.rb @@ -1,5 +1,5 @@ module JavascriptHelper def page_specific_javascript_tag(js) - javascript_include_tag asset_path(js), {integrity: true, "data-turbolinks-track" => true} + javascript_include_tag asset_path(js), { integrity: true, "data-turbolinks-track" => true } end end -- cgit v1.2.1 From 46b89a270f7eef15f6d8e41c79600795265054fb Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 24 Jun 2016 10:50:23 +0200 Subject: Fix tmp file being deleted after the request plus some cleanup and improved erroring for this situation --- CHANGELOG | 1 + app/controllers/import/gitlab_projects_controller.rb | 6 +++++- lib/gitlab/import_export/file_importer.rb | 6 +++++- lib/gitlab/import_export/importer.rb | 15 ++++++++++++--- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index bb408314ea8..2dfc23c4af6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -19,6 +19,7 @@ v 8.9.1 - Fix 404 when accessing pipelines as guest user on public projects - Fix mobile Safari bug where horizontal nav arrows would flicker on scroll - Fix in auto merge when pipeline is nil + - Fix GitLab import project deleting imported file straight after being uploaded v 8.9.0 - Fix builds API response not including commit data diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index f99aa490d3e..b521daa03aa 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -12,9 +12,13 @@ class Import::GitlabProjectsController < Import::BaseController return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive." }) end + imported_file = params[:file].path + "-import" + + FileUtils.copy_entry(params[:file].path, imported_file) + @project = Gitlab::ImportExport::ProjectCreator.new(project_params[:namespace_id], current_user, - File.expand_path(project_params[:file].path), + File.expand_path(imported_file), project_params[:path]).execute if @project.saved? diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb index 0e70d9282d5..82d1e1805c5 100644 --- a/lib/gitlab/import_export/file_importer.rb +++ b/lib/gitlab/import_export/file_importer.rb @@ -23,7 +23,11 @@ module Gitlab private def decompress_archive - untar_zxf(archive: @archive_file, dir: @shared.export_path) + result = untar_zxf(archive: @archive_file, dir: @shared.export_path) + + raise Projects::ImportService::Error.new("Unable to decompress #{@archive_file} into #{@shared.export_path}") unless result + + true end end end diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb index d209e04f7be..595b20a09bd 100644 --- a/lib/gitlab/import_export/importer.rb +++ b/lib/gitlab/import_export/importer.rb @@ -10,17 +10,22 @@ module Gitlab end def execute - Gitlab::ImportExport::FileImporter.import(archive_file: @archive_file, - shared: @shared) - if check_version! && [project_tree, repo_restorer, wiki_restorer, uploads_restorer].all?(&:restore) + if import_file && check_version! && [project_tree, repo_restorer, wiki_restorer, uploads_restorer].all?(&:restore) project_tree.restored_project else raise Projects::ImportService::Error.new(@shared.errors.join(', ')) end + + remove_import_file end private + def import_file + Gitlab::ImportExport::FileImporter.import(archive_file: @archive_file, + shared: @shared) + end + def check_version! Gitlab::ImportExport::VersionChecker.check!(shared: @shared) end @@ -59,6 +64,10 @@ module Gitlab def wiki_repo_path File.join(@shared.export_path, 'project.wiki.bundle') end + + def remove_import_file + FileUtils.rm_rf(@archive_file) + end end end end -- cgit v1.2.1 From c3b6eadc33272d08069244f196ffba21709114d5 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 24 Jun 2016 11:41:33 +0200 Subject: fix params --- app/controllers/import/gitlab_projects_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index b521daa03aa..513348c39af 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -12,9 +12,9 @@ class Import::GitlabProjectsController < Import::BaseController return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive." }) end - imported_file = params[:file].path + "-import" + imported_file = project_params[:file].path + "-import" - FileUtils.copy_entry(params[:file].path, imported_file) + FileUtils.copy_entry(project_params[:file].path, imported_file) @project = Gitlab::ImportExport::ProjectCreator.new(project_params[:namespace_id], current_user, -- cgit v1.2.1 From d470f3d1954a33d2ea6069fadcbb9810267b024f Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 21 Jun 2016 13:35:09 +0200 Subject: Support for rendering/redacting multiple documents This commit changes the way certain documents are rendered (currently only Notes) and how documents are redacted. Previously both rendering and redacting would run on a per document basis. The result of this was that for every document we'd have to run countless queries just to figure out if we could display a set of links or not. This commit changes things around so that redacting Markdown documents is no longer tied into the html-pipeline Gem. This in turn allows it to redact multiple documents in a single pass, thus reducing the number of queries needed. In turn rendering issue/merge request notes has been adjusted to take advantage of this new setup. Instead of rendering Markdown somewhere deep down in a view the Markdown is rendered and redacted in the controller (taking the current user and all that into account). This has been done in such a way that the "markdown()" helper method can still be used on its own. This particular commit also paves the way for caching rendered HTML on object level. Right now there's an accessor method Note#note_html which is used for setting/getting the rendered HTML. Once we cache HTML on row level we can simply change this field to be a column and call a "save" whenever needed and we're pretty much done. --- app/controllers/projects/issues_controller.rb | 6 +- .../projects/merge_requests_controller.rb | 26 ++++- app/controllers/projects/notes_controller.rb | 10 ++ app/models/note.rb | 4 + app/views/projects/notes/_note.html.haml | 2 +- config/initializers/metrics.rb | 4 + lib/banzai/filter/redactor_filter.rb | 29 +---- lib/banzai/note_renderer.rb | 22 ++++ lib/banzai/object_renderer.rb | 85 +++++++++++++++ lib/banzai/pipeline/relative_link_pipeline.rb | 11 ++ lib/banzai/redactor.rb | 69 ++++++++++++ spec/lib/banzai/note_renderer_spec.rb | 25 +++++ spec/lib/banzai/object_renderer_spec.rb | 120 +++++++++++++++++++++ spec/lib/banzai/redactor_spec.rb | 53 +++++++++ 14 files changed, 434 insertions(+), 32 deletions(-) create mode 100644 lib/banzai/note_renderer.rb create mode 100644 lib/banzai/object_renderer.rb create mode 100644 lib/banzai/pipeline/relative_link_pipeline.rb create mode 100644 lib/banzai/redactor.rb create mode 100644 spec/lib/banzai/note_renderer_spec.rb create mode 100644 spec/lib/banzai/object_renderer_spec.rb create mode 100644 spec/lib/banzai/redactor_spec.rb diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 4e2d3bebb2e..fbf8b01b7c2 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -62,8 +62,12 @@ class Projects::IssuesController < Projects::ApplicationController end def show + raw_notes = @issue.notes_with_associations.fresh + + @notes = Banzai::NoteRenderer. + render(raw_notes, @project, current_user, @path, @project_wiki, @ref) + @note = @project.notes.new(noteable: @issue) - @notes = @issue.notes.with_associations.fresh @noteable = @issue respond_to do |format| diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 089669841d3..79ba032996c 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -85,6 +85,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController @grouped_diff_notes = @merge_request.notes.grouped_diff_notes + Banzai::NoteRenderer.render( + @grouped_diff_notes.values.flatten, + @project, + current_user, + @path, + @project_wiki, + @ref + ) + respond_to do |format| format.html format.json { render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } } @@ -325,8 +334,21 @@ class Projects::MergeRequestsController < Projects::ApplicationController def define_show_vars # Build a note object for comment form @note = @project.notes.new(noteable: @merge_request) - @discussions = @merge_request.mr_and_commit_notes.inc_author_project_award_emoji.fresh.discussions - @notes = @discussions.flatten + + @discussions = @merge_request.mr_and_commit_notes. + inc_author_project_award_emoji. + fresh. + discussions + + @notes = Banzai::NoteRenderer.render( + @discussions.flatten, + @project, + current_user, + @path, + @project_wiki, + @ref + ) + @noteable = @merge_request # Get commits from repository diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 836f79ff080..e14fe26dde7 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -24,6 +24,10 @@ class Projects::NotesController < Projects::ApplicationController def create @note = Notes::CreateService.new(project, current_user, note_params).execute + if @note.is_a?(Note) + Banzai::NoteRenderer.render([@note], @project, current_user) + end + respond_to do |format| format.json { render json: note_json(@note) } format.html { redirect_back_or_default } @@ -33,6 +37,10 @@ class Projects::NotesController < Projects::ApplicationController def update @note = Notes::UpdateService.new(project, current_user, note_params).execute(note) + if @note.is_a?(Note) + Banzai::NoteRenderer.render([@note], @project, current_user) + end + respond_to do |format| format.json { render json: note_json(@note) } format.html { redirect_back_or_default } @@ -118,6 +126,8 @@ class Projects::NotesController < Projects::ApplicationController name: note.name } elsif note.valid? + Banzai::NoteRenderer.render([note], @project, current_user) + { valid: true, id: note.id, diff --git a/app/models/note.rb b/app/models/note.rb index e510525b89d..8db500a5219 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -6,6 +6,10 @@ class Note < ActiveRecord::Base include Awardable include Importable + # Attribute containing rendered and redacted Markdown as generated by + # Banzai::ObjectRenderer. + attr_accessor :note_html + default_value_for :system, false attr_mentionable :note, pipeline: :note diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index c04d291412c..a5e163b91e9 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -32,7 +32,7 @@ .note-body{class: note_editable ? 'js-task-list-container' : ''} .note-text = preserve do - = markdown(note.note, pipeline: :note, cache_key: [note, "note"], author: note.author) + = note.note_html = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) - if note_editable = render 'projects/notes/edit_form', note: note diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb index d159f4eded2..75f89d524e7 100644 --- a/config/initializers/metrics.rb +++ b/config/initializers/metrics.rb @@ -113,6 +113,10 @@ if Gitlab::Metrics.enabled? config.instrument_methods(Banzai::Renderer) config.instrument_methods(Banzai::Querying) + config.instrument_instance_methods(Banzai::ObjectRenderer) + config.instrument_instance_methods(Banzai::Redactor) + config.instrument_methods(Banzai::NoteRenderer) + [Issuable, Mentionable, Participable].each do |klass| config.instrument_instance_methods(klass) config.instrument_instance_methods(klass::ClassMethods) diff --git a/lib/banzai/filter/redactor_filter.rb b/lib/banzai/filter/redactor_filter.rb index c753a84a20d..c59a80dd1c7 100644 --- a/lib/banzai/filter/redactor_filter.rb +++ b/lib/banzai/filter/redactor_filter.rb @@ -7,40 +7,13 @@ module Banzai # class RedactorFilter < HTML::Pipeline::Filter def call - nodes = Querying.css(doc, 'a.gfm[data-reference-type]') - visible = nodes_visible_to_user(nodes) - - nodes.each do |node| - unless visible.include?(node) - # The reference should be replaced by the original text, - # which is not always the same as the rendered text. - text = node.attr('data-original') || node.text - node.replace(text) - end - end + Redactor.new(project, current_user).redact([doc]) doc end private - def nodes_visible_to_user(nodes) - per_type = Hash.new { |h, k| h[k] = [] } - visible = Set.new - - nodes.each do |node| - per_type[node.attr('data-reference-type')] << node - end - - per_type.each do |type, nodes| - parser = Banzai::ReferenceParser[type].new(project, current_user) - - visible.merge(parser.nodes_visible_to_user(current_user, nodes)) - end - - visible - end - def current_user context[:current_user] end diff --git a/lib/banzai/note_renderer.rb b/lib/banzai/note_renderer.rb new file mode 100644 index 00000000000..bab6a9934d1 --- /dev/null +++ b/lib/banzai/note_renderer.rb @@ -0,0 +1,22 @@ +module Banzai + module NoteRenderer + # Renders a collection of Note instances. + # + # notes - The notes to render. + # project - The project to use for rendering/redacting. + # user - The user viewing the notes. + # path - The request path. + # wiki - The project's wiki. + # git_ref - The current Git reference. + def self.render(notes, project, user = nil, path = nil, wiki = nil, git_ref = nil) + renderer = ObjectRenderer.new(project, + user, + requested_path: path, + project_wiki: wiki, + ref: git_ref, + pipeline: :note) + + renderer.render(notes, :note) + end + end +end diff --git a/lib/banzai/object_renderer.rb b/lib/banzai/object_renderer.rb new file mode 100644 index 00000000000..f0e4f28bf12 --- /dev/null +++ b/lib/banzai/object_renderer.rb @@ -0,0 +1,85 @@ +module Banzai + # Class for rendering multiple objects (e.g. Note instances) in a single pass. + # + # Rendered Markdown is stored in an attribute in every object based on the + # name of the attribute containing the Markdown. For example, when the + # attribute `note` is rendered the HTML is stored in `note_html`. + class ObjectRenderer + attr_reader :project, :user + + # Make sure to set the appropriate pipeline in the `raw_context` attribute + # (e.g. `:note` for Note instances). + # + # project - A Project to use for rendering and redacting Markdown. + # user - The user viewing the Markdown/HTML documents, if any. + # context - A Hash containing extra attributes to use in the rendering + # pipeline. + def initialize(project, user = nil, raw_context = {}) + @project = project + @user = user + @raw_context = raw_context + end + + # Renders and redacts an Array of objects. + # + # objects - The objects to render + # attribute - The attribute containing the raw Markdown to render. + # + # Returns the same input objects. + def render(objects, attribute) + documents = render_objects(objects, attribute) + redacted = redact_documents(documents) + + objects.each_with_index do |object, index| + object.__send__("#{attribute}_html=", redacted.fetch(index)) + end + + objects + end + + # Renders the attribute of every given object. + def render_objects(objects, attribute) + objects.map do |object| + render_attribute(object, attribute) + end + end + + # Redacts the list of documents. + # + # Returns an Array containing the redacted documents. + def redact_documents(documents) + redactor = Redactor.new(project, user) + + redactor.redact(documents).map do |document| + document.to_html.html_safe + end + end + + # Returns a Banzai context for the given object and attribute. + def context_for(object, attribute) + context = base_context.merge(cache_key: [object, attribute]) + + if object.respond_to?(:author) + context[:author] = object.author + end + + context + end + + # Renders the attribute of an object. + # + # Returns a `Nokogiri::HTML::Document`. + def render_attribute(object, attribute) + context = context_for(object, attribute) + + string = object.__send__(attribute) + html = Banzai.render(string, context) + + Banzai::Pipeline[:relative_link].to_document(html, context) + end + + def base_context + @base_context ||= @raw_context.merge(current_user: user, project: project) + end + end +end diff --git a/lib/banzai/pipeline/relative_link_pipeline.rb b/lib/banzai/pipeline/relative_link_pipeline.rb new file mode 100644 index 00000000000..270990e7ab4 --- /dev/null +++ b/lib/banzai/pipeline/relative_link_pipeline.rb @@ -0,0 +1,11 @@ +module Banzai + module Pipeline + class RelativeLinkPipeline < BasePipeline + def self.filters + FilterArray[ + Filter::RelativeLinkFilter + ] + end + end + end +end diff --git a/lib/banzai/redactor.rb b/lib/banzai/redactor.rb new file mode 100644 index 00000000000..ffd267d5e9a --- /dev/null +++ b/lib/banzai/redactor.rb @@ -0,0 +1,69 @@ +module Banzai + # Class for removing Markdown references a certain user is not allowed to + # view. + class Redactor + attr_reader :user, :project + + # project - A Project to use for redacting links. + # user - The currently logged in user (if any). + def initialize(project, user = nil) + @project = project + @user = user + end + + # Redacts the references in the given Array of documents. + # + # This method modifies the given documents in-place. + # + # documents - A list of HTML documents containing references to redact. + # + # Returns the documents passed as the first argument. + def redact(documents) + nodes = documents.flat_map do |document| + Querying.css(document, 'a.gfm[data-reference-type]') + end + + redact_nodes(nodes) + + documents + end + + # Redacts the given nodes + # + # nodes - An Array of HTML nodes to redact. + def redact_nodes(nodes) + visible = nodes_visible_to_user(nodes) + + nodes.each do |node| + unless visible.include?(node) + # The reference should be replaced by the original text, + # which is not always the same as the rendered text. + text = node.attr('data-original') || node.text + node.replace(text) + end + end + end + + # Returns the nodes visible to the current user. + # + # nodes - The input nodes to check. + # + # Returns a new Array containing the visible nodes. + def nodes_visible_to_user(nodes) + per_type = Hash.new { |h, k| h[k] = [] } + visible = Set.new + + nodes.each do |node| + per_type[node.attr('data-reference-type')] << node + end + + per_type.each do |type, nodes| + parser = Banzai::ReferenceParser[type].new(project, user) + + visible.merge(parser.nodes_visible_to_user(user, nodes)) + end + + visible + end + end +end diff --git a/spec/lib/banzai/note_renderer_spec.rb b/spec/lib/banzai/note_renderer_spec.rb new file mode 100644 index 00000000000..98f76f36fd5 --- /dev/null +++ b/spec/lib/banzai/note_renderer_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Banzai::NoteRenderer do + describe '.render' do + it 'renders a Note' do + note = double(:note) + project = double(:project) + wiki = double(:wiki) + user = double(:user) + + expect(Banzai::ObjectRenderer).to receive(:new). + with(project, user, + requested_path: 'foo', + project_wiki: wiki, + ref: 'bar', + pipeline: :note). + and_call_original + + expect_any_instance_of(Banzai::ObjectRenderer). + to receive(:render).with([note], :note) + + described_class.render([note], project, user, 'foo', wiki, 'bar') + end + end +end diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb new file mode 100644 index 00000000000..44256b32bdc --- /dev/null +++ b/spec/lib/banzai/object_renderer_spec.rb @@ -0,0 +1,120 @@ +require 'spec_helper' + +describe Banzai::ObjectRenderer do + let(:project) { create(:empty_project) } + let(:user) { project.owner } + + describe '#render' do + it 'renders and redacts an Array of objects' do + renderer = described_class.new(project, user) + object = double(:object, note: 'hello', note_html: nil) + + expect(renderer).to receive(:render_objects).with([object], :note). + and_call_original + + expect(renderer).to receive(:redact_documents). + with(an_instance_of(Array)). + and_call_original + + expect(object).to receive(:note_html=).with('

hello

') + + renderer.render([object], :note) + end + end + + describe '#render_objects' do + it 'renders an Array of objects' do + object = double(:object, note: 'hello') + renderer = described_class.new(project, user) + + expect(renderer).to receive(:render_attribute).with(object, :note). + and_call_original + + rendered = renderer.render_objects([object], :note) + + expect(rendered).to be_an_instance_of(Array) + expect(rendered[0]).to be_an_instance_of(Nokogiri::HTML::DocumentFragment) + end + end + + describe '#redact_documents' do + it 'redacts a set of documents and returns them as an Array of Strings' do + doc = Nokogiri::HTML.fragment('

hello

') + renderer = described_class.new(project, user) + + expect_any_instance_of(Banzai::Redactor).to receive(:redact). + with([doc]). + and_call_original + + redacted = renderer.redact_documents([doc]) + + expect(redacted).to eq(['

hello

']) + end + end + + describe '#context_for' do + let(:object) { double(:object, note: 'hello') } + let(:renderer) { described_class.new(project, user) } + + it 'returns a Hash' do + expect(renderer.context_for(object, :note)).to be_an_instance_of(Hash) + end + + it 'includes the cache key' do + context = renderer.context_for(object, :note) + + expect(context[:cache_key]).to eq([object, :note]) + end + + context 'when the object responds to "author"' do + it 'includes the author in the context' do + expect(object).to receive(:author).and_return('Alice') + + context = renderer.context_for(object, :note) + + expect(context[:author]).to eq('Alice') + end + end + + context 'when the object does not respond to "author"' do + it 'does not include the author in the context' do + context = renderer.context_for(object, :note) + + expect(context.key?(:author)).to eq(false) + end + end + end + + describe '#render_attribute' do + it 'renders the attribute of an object' do + object = double(:doc, note: 'hello') + renderer = described_class.new(project, user, pipeline: :note) + doc = renderer.render_attribute(object, :note) + + expect(doc).to be_an_instance_of(Nokogiri::HTML::DocumentFragment) + expect(doc.to_html).to eq('

hello

') + end + end + + describe '#base_context' do + let(:context) do + described_class.new(project, user, pipeline: :note).base_context + end + + it 'returns a Hash' do + expect(context).to be_an_instance_of(Hash) + end + + it 'includes the custom attributes' do + expect(context[:pipeline]).to eq(:note) + end + + it 'includes the current user' do + expect(context[:current_user]).to eq(user) + end + + it 'includes the current project' do + expect(context[:project]).to eq(project) + end + end +end diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb new file mode 100644 index 00000000000..488f465bcda --- /dev/null +++ b/spec/lib/banzai/redactor_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe Banzai::Redactor do + let(:user) { build(:user) } + let(:project) { build(:empty_project) } + let(:redactor) { described_class.new(project, user) } + + describe '#redact' do + it 'redacts an Array of documents' do + doc1 = Nokogiri::HTML. + fragment('foo') + + doc2 = Nokogiri::HTML. + fragment('bar') + + expect(redactor).to receive(:nodes_visible_to_user).and_return([]) + + expect(redactor.redact([doc1, doc2])).to eq([doc1, doc2]) + + expect(doc1.to_html).to eq('foo') + expect(doc2.to_html).to eq('bar') + end + end + + describe '#redact_nodes' do + it 'redacts an Array of nodes' do + doc = Nokogiri::HTML.fragment('foo') + node = doc.children[0] + + expect(redactor).to receive(:nodes_visible_to_user). + with([node]). + and_return(Set.new) + + redactor.redact_nodes([node]) + + expect(doc.to_html).to eq('foo') + end + end + + describe '#nodes_visible_to_user' do + it 'returns a Set containing the visible nodes' do + doc = Nokogiri::HTML.fragment('') + node = doc.children[0] + + expect_any_instance_of(Banzai::ReferenceParser::IssueParser). + to receive(:nodes_visible_to_user). + with(user, [node]). + and_return([node]) + + expect(redactor.nodes_visible_to_user([node])).to eq(Set.new([node])) + end + end +end -- cgit v1.2.1 From 7ee84f1f25d25e131079b468722c9951d85fa983 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 24 Jun 2016 12:36:22 +0200 Subject: Added changelog entry for redacting improvements [ci skip] --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 2dfc23c4af6..19782969b85 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,7 @@ v 8.9.1 - Improve performance of searching repository tags by name by using a memorized tag array - Fix false truncated warnings with ISO-8559 files - Fix unwanted label unassignment when doing bulk action on issues page + - Improve performance of redacting Markdown references - Fix 404 when accessing pipelines as guest user on public projects - Fix mobile Safari bug where horizontal nav arrows would flicker on scroll - Fix in auto merge when pipeline is nil -- cgit v1.2.1 From 59b5bb033409901dc8e1fa7fa28c3a0fa38f495a Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Fri, 24 Jun 2016 07:27:58 -0700 Subject: Disable the email checking part of the standard Health Check The email check used in the Heath Check doesn't properly make use of enough of the SMTP config options to be able to properly test the STMP connection, and as a result could cause a failure. In order to fix it we have overwritten the email_configured? method in the health check so that it does not check email status during the standard health check. --- CHANGELOG | 1 + config/initializers/health_check.rb | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index fcfd0a21be3..fc76cbe39c9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ v 8.10.0 (unreleased) - Add Sidekiq queue duration to transaction metrics. - Fix MR-auto-close text added to description. !4836 - Fix pagination when sorting by columns with lots of ties (like priority) + - Exclude email check from the standard health check - Implement Subresource Integrity for CSS and JavaScript assets. This prevents malicious assets from loading in the case of a CDN compromise. - Fix user creation with stronger minimum password requirements !4054 (nathan-pmt) diff --git a/config/initializers/health_check.rb b/config/initializers/health_check.rb index 79e2d23ab2e..3b027753334 100644 --- a/config/initializers/health_check.rb +++ b/config/initializers/health_check.rb @@ -1,3 +1,16 @@ +# Email forcibly included in the standard checks, but the email health check +# doesn't support the full range of SMTP options, which can result in failures +# for valid SMTP configurations. +# Overwrite the HealthCheck's detection of whether email is configured +# in order to avoid the email check during standard checks +module HealthCheck + class Utils + def self.mailer_configured? + false + end + end +end + HealthCheck.setup do |config| config.standard_checks = ['database', 'migrations', 'cache'] end -- cgit v1.2.1 From 4cfb1af0b716501700e0e62ee0e514f498aa699c Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Fri, 24 Jun 2016 08:47:08 -0700 Subject: Set the health_check mailer full checks to be the same as the standard checks There was nothing additional in the full checks that we want to run (email, custom) --- config/initializers/health_check.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/initializers/health_check.rb b/config/initializers/health_check.rb index 3b027753334..6796407d4e6 100644 --- a/config/initializers/health_check.rb +++ b/config/initializers/health_check.rb @@ -13,4 +13,5 @@ end HealthCheck.setup do |config| config.standard_checks = ['database', 'migrations', 'cache'] + config.full_checks = ['database', 'migrations', 'cache'] end -- cgit v1.2.1 From 67e83543b3ab0b858dfb32f68ea03b3dd6d0b052 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 7 Jun 2016 19:40:38 -0700 Subject: Add debugging tips with gdb [ci skip] --- doc/README.md | 1 + doc/administration/troubleshooting/debug.md | 120 +++++++++++++++++ .../troubleshooting/gdb-stuck-ruby.txt | 142 +++++++++++++++++++++ 3 files changed, 263 insertions(+) create mode 100644 doc/administration/troubleshooting/debug.md create mode 100644 doc/administration/troubleshooting/gdb-stuck-ruby.txt diff --git a/doc/README.md b/doc/README.md index f1283cea0ad..be0d17084c7 100644 --- a/doc/README.md +++ b/doc/README.md @@ -44,6 +44,7 @@ - [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast. - [GitLab Performance Monitoring](monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics. - [Monitoring uptime](monitoring/health_check.md) Check the server status using the health check endpoint. +- [Debugging Tips](administration/troubleshooting/debug.md) Tips to debug problems when things go wrong - [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs. - [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability. - [Container Registry](administration/container_registry.md) Configure Docker Registry with GitLab. diff --git a/doc/administration/troubleshooting/debug.md b/doc/administration/troubleshooting/debug.md new file mode 100644 index 00000000000..e5701b86cf3 --- /dev/null +++ b/doc/administration/troubleshooting/debug.md @@ -0,0 +1,120 @@ +# Debugging Tips + +Sometimes things don't work the way they should. Here are some tips on debugging issues out +in production. + +## The GNU Project Debugger (gdb) + +`gdb` is a must-have tool for debugging issues. To install on Ubuntu/Debian: + +``` +sudo apt-get install gdb +``` + +On CentOS: + +``` +sudo yum install gdb +``` + +## Common Problems + +Many of the tips to diagnose issues below apply to many different situations. We'll use one +concrete example to illustrate what you can do to learn what is going wrong. + +### 502 Gateway Timeout after unicorn spins at 100% CPU + +This error occurs when the Web server times out (default: 60 s) after not +hearing back from the unicorn worker. If the CPU spins to 100% while this in +progress, there may be something taking longer than it should. + +To fix this issue, we first need to figure out what is happening. The +following tips are only recommended if you do NOT mind users being affected by +downtime. Otherwise skip to the next section. + +1. Load the problematic URL +1. Run `sudo gdb -p ` to attach to the unicorn process. +1. In the gdb window, type: + + ``` + call (void) rb_backtrace() + ``` + +1. This forces the process to generate a Ruby backtrace. Check + `/var/log/gitlab/unicorn/unicorn_stderr.log` for the backtace. For example, you may see: + + ```ruby + from /opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/metrics/sampler.rb:33:in `block in start' + from /opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/metrics/sampler.rb:33:in `loop' + from /opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/metrics/sampler.rb:36:in `block (2 levels) in start' + from /opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/metrics/sampler.rb:44:in `sample' + from /opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/metrics/sampler.rb:68:in `sample_objects' + from /opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/metrics/sampler.rb:68:in `each_with_object' + from /opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/metrics/sampler.rb:68:in `each' + from /opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/metrics/sampler.rb:69:in `block in sample_objects' + from /opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/metrics/sampler.rb:69:in `name' + ``` + +1. To see the current threads, run: + + ``` + apply all thread bt + ``` + +1. Once you're done debugging with `gdb`, be sure to detach from the process and exit: + + ``` + detach + exit + ``` + +Note that if the unicorn process terminates before you are able to run these +commands, gdb will report an error. To buy more time, you can always raise the +Unicorn timeout. For omnibus users, you can edit `/etc/gitlab/gitlab.rb` and +increase it from 60 seconds to 300: + +```ruby +unicorn['worker_timeout'] = 300 +``` + +For source installations, edit `config/unicorn.rb`. + +[Reconfigure] GitLab for the changes to take effect. + +[Reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure + +#### Troubleshooting without affecting other users + +The previous section attached to a running unicorn process, and this may have +undesirable effects for users trying to access GitLab during this time. If you +are concerned about affecting others during a production system, you can run a +separate Rails process to debug the issue: + +1. Log in to your GitLab account. +1. Copy the URL that is causing problems (e.g. https://gitlab.com/ABC). +1. Obtain the private token for your user (Profile Settings -> Account). +1. Bring up the GitLab Rails console. For omnibus users, run: + + ```` + sudo gitlab-rails console + ``` + +1. At the Rails console, run: + + ```ruby + [1] pry(main)> app.get '/private_token?' + ``` + + For example: + + ```ruby + [1] pry(main)> app.get 'https://gitlab.com/gitlab-org/gitlab-ce/issues/1?private_token=123456' + ``` + +1. In a new window, run `top`. It should show this ruby process using 100% CPU. Write down the PID. +1. Follow step 2 from the previous section on using gdb. + +# More information + +* [Debugging Stuck Ruby Processes](https://blog.newrelic.com/2013/04/29/debugging-stuck-ruby-processes-what-to-do-before-you-kill-9/) +* [Cheatsheet of using gdb and ruby processes](gdb-stuck-ruby.txt) diff --git a/doc/administration/troubleshooting/gdb-stuck-ruby.txt b/doc/administration/troubleshooting/gdb-stuck-ruby.txt new file mode 100644 index 00000000000..13d5dfcffa4 --- /dev/null +++ b/doc/administration/troubleshooting/gdb-stuck-ruby.txt @@ -0,0 +1,142 @@ +# Here's the script I'll use to demonstrate - it just loops forever: + +$ cat test.rb +#!/usr/bin/env ruby + +loop do + sleep 1 +end + +# Now, I'll start the script in the background, and redirect stdout and stderr +# to /dev/null: + +$ ruby ./test.rb >/dev/null 2>/dev/null & +[1] 1343 + +# Next, I'll grab the PID of the script (1343): + +$ ps aux | grep test.rb +vagrant 1343 0.0 0.4 3884 1652 pts/0 S 14:42 0:00 ruby ./test.rb +vagrant 1345 0.0 0.2 4624 852 pts/0 S+ 14:42 0:00 grep --color=auto test.rb + +# Now I start gdb. Note that I'm using sudo here. This may or may not be +# necessary in your setup. I'd try without sudo first, and fall back to adding +# it if the next step fails: + +$ sudo gdb +GNU gdb (Ubuntu/Linaro 7.4-2012.04-0ubuntu2.1) 7.4-2012.04 +Copyright (C) 2012 Free Software Foundation, Inc. +License GPLv3+: GNU GPL version 3 or later +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. Type "show copying" +and "show warranty" for details. +This GDB was configured as "i686-linux-gnu". +For bug reporting instructions, please see: +. + +# OK, now I'm in gdb, and I want to instruct it to attach to our Ruby process. +# I can do that using the 'attach' command, which takes a PID (the one we +# gathered above): + +(gdb) attach 1343 +Attaching to process 1343 +Reading symbols from /opt/vagrant_ruby/bin/ruby...done. +Reading symbols from /lib/i386-linux-gnu/librt.so.1...(no debugging symbols found)...done. +Loaded symbols for /lib/i386-linux-gnu/librt.so.1 +Reading symbols from /lib/i386-linux-gnu/libdl.so.2...(no debugging symbols found)...done. +Loaded symbols for /lib/i386-linux-gnu/libdl.so.2 +Reading symbols from /lib/i386-linux-gnu/libcrypt.so.1...(no debugging symbols found)...done. +Loaded symbols for /lib/i386-linux-gnu/libcrypt.so.1 +Reading symbols from /lib/i386-linux-gnu/libm.so.6...(no debugging symbols found)...done. +Loaded symbols for /lib/i386-linux-gnu/libm.so.6 +Reading symbols from /lib/i386-linux-gnu/libc.so.6...(no debugging symbols found)...done. +Loaded symbols for /lib/i386-linux-gnu/libc.so.6 +Reading symbols from /lib/i386-linux-gnu/libpthread.so.0...(no debugging symbols found)...done. +[Thread debugging using libthread_db enabled] +Using host libthread_db library "/lib/i386-linux-gnu/libthread_db.so.1". +Loaded symbols for /lib/i386-linux-gnu/libpthread.so.0 +Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done. +Loaded symbols for /lib/ld-linux.so.2 +0xb770c424 in __kernel_vsyscall () + +# Great, now gdb is attached to the target process. If the step above fails, try +# going back and running gdb under sudo. The next thing I want to do is gather +# C-level backtraces from all threads in the process. The following command +# stands for 'thread apply all backtrace': + +(gdb) t a a bt + +Thread 1 (Thread 0xb74d76c0 (LWP 1343)): +#0 0xb770c424 in __kernel_vsyscall () +#1 0xb75d7abd in select () from /lib/i386-linux-gnu/libc.so.6 +#2 0x08069c56 in rb_thread_wait_for (time=...) at eval.c:11376 +#3 0x080a20fd in rb_f_sleep (argc=1, argv=0xbf85f490) at process.c:1633 +#4 0x0805e0e2 in call_cfunc (argv=0xbf85f490, argc=1, len=-1, recv=3075299660, func=0x80a20b0 ) + at eval.c:5778 +#5 rb_call0 (klass=3075304600, recv=3075299660, id=9393, oid=9393, argc=1, argv=0xbf85f490, body=0xb74c85a8, flags=2) + at eval.c:5928 +#6 0x0805e35d in rb_call (klass=3075304600, recv=3075299660, mid=9393, argc=1, argv=0xbf85f490, scope=1, + self=) at eval.c:6176 +#7 0x080651ec in rb_eval (self=3075299660, n=0xb74c4e1c) at eval.c:3521 +#8 0x0805c31c in rb_yield_0 (val=6, self=3075299660, klass=, flags=0, avalue=0) at eval.c:5095 +#9 0x0806a1e5 in loop_i () at eval.c:5227 +#10 0x08058dbd in rb_rescue2 (b_proc=0x806a1c0 , data1=0, r_proc=0, data2=0) at eval.c:5491 +#11 0x08058f28 in rb_f_loop () at eval.c:5252 +#12 0x0805e0c1 in call_cfunc (argv=0x0, argc=0, len=0, recv=3075299660, func=0x8058ef0 ) at eval.c:5781 +#13 rb_call0 (klass=3075304600, recv=3075299660, id=4121, oid=4121, argc=0, argv=0x0, body=0xb74d4dbc, flags=2) + at eval.c:5928 +#14 0x0805e35d in rb_call (klass=3075304600, recv=3075299660, mid=4121, argc=0, argv=0x0, scope=1, self=) + at eval.c:6176 +#15 0x080651ec in rb_eval (self=3075299660, n=0xb74c4dcc) at eval.c:3521 +#16 0x080662c6 in rb_eval (self=3075299660, n=0xb74c4de0) at eval.c:3236 +#17 0x08068ee4 in ruby_exec_internal () at eval.c:1654 +#18 0x08068f24 in ruby_exec () at eval.c:1674 +#19 0x0806b2cd in ruby_run () at eval.c:1684 +#20 0x08053771 in main (argc=2, argv=0xbf860204, envp=0xbf860210) at main.c:48 + +# C backtraces are sometimes sufficient, but often Ruby backtraces are necessary +# for debugging as well. Ruby has a built-in function called rb_backtrace() that +# we can use to dump out a Ruby backtrace, but it prints to stdout or stderr +# (depending on your Ruby version), which might have been redirected to a file +# or to /dev/null (as in our example) when the process started up. +# +# To get aroundt this, we'll do a little trick and redirect the target process's +# stdout and stderr to the current TTY, so that any output from the process +# will appear directly on our screen. + +# First, let's close the existing file descriptors for stdout and stderr +# (FD 1 and 2, respectively): +(gdb) call (void) close(1) +(gdb) call (void) close(2) + +# Next, we need to figure out the device name for the current TTY: +(gdb) shell tty +/dev/pts/0 + +# OK, now we can pass the device name obtained above to open() and attach +# file descriptors 1 and 2 back to the current TTY with these calls: + +(gdb) call (int) open("/dev/pts/0", 2, 0) +$1 = 1 +(gdb) call (int) open("/dev/pts/0", 2, 0) +$2 = 2 + +# Finally, we call rb_backtrace() in order to dump the Ruby backtrace: + +(gdb) call (void) rb_backtrace() + from ./test.rb:4:in `sleep' + from ./test.rb:4 + from ./test.rb:3:in `loop' + from ./test.rb:3 + +# And here's how we get out of gdb. Once you've quit, you'll probably want to +# clean up the stuck process by killing it. + +(gdb) quit +A debugging session is active. + + Inferior 1 [process 1343] will be detached. + +Quit anyway? (y or n) y +Detaching from program: /opt/vagrant_ruby/bin/ruby, process 1343 +$ -- cgit v1.2.1 From 2e33e7044cdf225b0af4d4cbedf8e582ef2e61ea Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 24 Jun 2016 18:19:15 +0200 Subject: Fixed award emoji changelog entry This is meant to go in 8.9.1, not 8.10.0. [ci skip] --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 5e115a8bb17..2c139336b15 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,7 +5,6 @@ v 8.10.0 (unreleased) - Wrap code blocks on Activies and Todos page. !4783 (winniehell) - Add Sidekiq queue duration to transaction metrics. - Fix MR-auto-close text added to description. !4836 - - Eager load award emoji on notes - Fix pagination when sorting by columns with lots of ties (like priority) - Implement Subresource Integrity for CSS and JavaScript assets. This prevents malicious assets from loading in the case of a CDN compromise. - Fix user creation with stronger minimum password requirements !4054 (nathan-pmt) @@ -16,6 +15,7 @@ v 8.9.1 - Fix GitLab project import issues related to notes and builds - Improve performance of searching repository tags by name by using a memorized tag array - Fix false truncated warnings with ISO-8559 files + - Eager load award emoji on notes - Fix unwanted label unassignment when doing bulk action on issues page - Improve performance of redacting Markdown references - Fix 404 when accessing pipelines as guest user on public projects -- cgit v1.2.1 From e3d464d82a15cb524d066c2ba5d0973efbbfeeef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 24 Jun 2016 18:13:56 +0200 Subject: Fix a wrong MR status when merge_when_build_succeeds & project.only_allow_merge_if_build_succeeds are true MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The issue was that `MergeRequest#mergeable?` returns false when the CI state is not success and project.only_allow_merge_if_build_succeeds is true. In this case `Projects::MergeRequestsController#merge` would return the `:failed` status when enabling `merge_when_build_succeeds`, thus leading to a weird state and the MR never automatically merged. The fix is to disable the CI state check in the controller safeguard that early return the `:failed` status. Signed-off-by: Rémy Coutable --- CHANGELOG | 1 + app/controllers/projects/merge_requests_controller.rb | 9 ++++++++- app/models/merge_request.rb | 8 ++++---- spec/controllers/projects/merge_requests_controller_spec.rb | 12 ++++++++++++ 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2c139336b15..26042860dbc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -22,6 +22,7 @@ v 8.9.1 - Fix mobile Safari bug where horizontal nav arrows would flicker on scroll - Fix in auto merge when pipeline is nil - Fix GitLab import project deleting imported file straight after being uploaded + - Fix a wrong MR status when merge_when_build_succeeds & project.only_allow_merge_if_build_succeeds are true v 8.9.0 - Fix builds API response not including commit data diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 79ba032996c..39c8ba40ca2 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -199,7 +199,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController def merge return access_denied! unless @merge_request.can_be_merged_by?(current_user) - unless @merge_request.mergeable? + # Disable the CI check if merge_when_build_succeeds is enabled since we have + # to wait until CI completes to know + unless @merge_request.mergeable?(skip_ci_check: merge_when_build_succeeds_active?) @status = :failed return end @@ -395,4 +397,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController def ensure_ref_fetched @merge_request.ensure_ref_fetched end + + def merge_when_build_succeeds_active? + params[:merge_when_build_succeeds].present? && + @merge_request.pipeline && @merge_request.pipeline.active? + end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 36bc98bdb1e..f5c5b7c1306 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -264,19 +264,19 @@ class MergeRequest < ActiveRecord::Base self.title.sub(WIP_REGEX, "") end - def mergeable? - return false unless mergeable_state? + def mergeable?(skip_ci_check: false) + return false unless mergeable_state?(skip_ci_check: skip_ci_check) check_if_can_be_merged can_be_merged? end - def mergeable_state? + def mergeable_state?(skip_ci_check: false) return false unless open? return false if work_in_progress? return false if broken? - return false unless mergeable_ci_state? + return false unless skip_ci_check || mergeable_ci_state? true end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 4b408c03703..5e66106122c 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -264,6 +264,18 @@ describe Projects::MergeRequestsController do merge_when_build_succeeds end + + context 'when project.only_allow_merge_if_build_succeeds? is true' do + before do + project.update_column(:only_allow_merge_if_build_succeeds, true) + end + + it 'returns :merge_when_build_succeeds' do + merge_when_build_succeeds + + expect(assigns(:status)).to eq(:merge_when_build_succeeds) + end + end end end end -- cgit v1.2.1 From 83a9e82ff6ecd49b7af5dcdac26090b70666feb8 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Fri, 24 Jun 2016 10:43:20 -0600 Subject: Upgrade sentry-raven from 0.15.6 to 1.1.0. Changelog: https://github.com/getsentry/raven-ruby/releases --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index bae00090fda..895970dcca3 100644 --- a/Gemfile +++ b/Gemfile @@ -234,7 +234,7 @@ gem 'net-ssh', '~> 3.0.1' gem 'base32', '~> 0.3.0' # Sentry integration -gem 'sentry-raven', '~> 0.15' +gem 'sentry-raven', '~> 1.1.0' gem 'premailer-rails', '~> 1.9.0' diff --git a/Gemfile.lock b/Gemfile.lock index 7ea7ea95c1b..3f3ceb667b5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -656,7 +656,7 @@ GEM activesupport (>= 3.1, < 4.3) select2-rails (3.5.9.3) thor (~> 0.14) - sentry-raven (0.15.6) + sentry-raven (1.1.0) faraday (>= 0.7.6) settingslogic (2.0.9) sexp_processor (4.7.0) @@ -952,7 +952,7 @@ DEPENDENCIES sdoc (~> 0.3.20) seed-fu (~> 2.3.5) select2-rails (~> 3.5.9) - sentry-raven (~> 0.15) + sentry-raven (~> 1.1.0) settingslogic (~> 2.0.9) sham_rack shoulda-matchers (~> 2.8.0) -- cgit v1.2.1 From c391a72c18e7081cfbc1b80627663fdfcf4bbfe6 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Wed, 25 May 2016 18:34:40 -0400 Subject: Fix issue being ordered twice and callback when moving between states --- CHANGELOG | 1 + app/assets/javascripts/milestone.js.coffee | 5 +++-- app/controllers/projects/issues_controller.rb | 2 +- app/models/issue.rb | 4 ++++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4ebf0440cdd..e7838e7a8f1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -92,6 +92,7 @@ v 8.9.0 - Links from a wiki page to other wiki pages should be rewritten as expected - Add option to project to only allow merge requests to be merged if the build succeeds (Rui Santos) - Added navigation shortcuts to the project pipelines, milestones, builds and forks page. !4393 + - Fix changing issue state columns in milestone view - Fix issues filter when ordering by milestone - Disable SAML account unlink feature - Added artifacts:when to .gitlab-ci.yml - this requires GitLab Runner 1.3 diff --git a/app/assets/javascripts/milestone.js.coffee b/app/assets/javascripts/milestone.js.coffee index 0037a3a21c2..0fd64e9384e 100644 --- a/app/assets/javascripts/milestone.js.coffee +++ b/app/assets/javascripts/milestone.js.coffee @@ -81,8 +81,9 @@ class @Milestone stop: (event, ui) -> $(".issues-sortable-list").css "min-height", "0px" update: (event, ui) -> - data = $(this).sortable("serialize") - Milestone.sortIssues(data) + if this == ui.item.parent()[0] + data = $(this).sortable("serialize") + Milestone.sortIssues(data) receive: (event, ui) -> new_state = $(this).data('state') diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index fbf8b01b7c2..80dbabf51a5 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -116,7 +116,7 @@ class Projects::IssuesController < Projects::ApplicationController end end format.json do - render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }) + render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: :saved) end end end diff --git a/app/models/issue.rb b/app/models/issue.rb index 3c5859194b4..421fd44dffc 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -190,4 +190,8 @@ class Issue < ActiveRecord::Base def overdue? due_date.try(:past?) || false end + + def saved + valid? + end end -- cgit v1.2.1 From bade71ba5a25dfa6ca49c84650a7178b9275bf63 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Wed, 25 May 2016 19:08:48 -0400 Subject: Add specs and improve coffescript sortable binding function --- CHANGELOG | 2 +- app/assets/javascripts/milestone.js.coffee | 60 ++++++++++++------------- app/controllers/projects/issues_controller.rb | 3 +- app/models/issue.rb | 4 -- app/views/shared/milestones/_issuable.html.haml | 9 ++-- 5 files changed, 37 insertions(+), 41 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e7838e7a8f1..d32c1fd8492 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,7 @@ v 8.10.0 (unreleased) - Fix pagination when sorting by columns with lots of ties (like priority) - Exclude email check from the standard health check - Implement Subresource Integrity for CSS and JavaScript assets. This prevents malicious assets from loading in the case of a CDN compromise. + - Fix changing issue state columns in milestone view - Fix user creation with stronger minimum password requirements !4054 (nathan-pmt) - Add API endpoint for a group issues !4520 (mahcsig) @@ -92,7 +93,6 @@ v 8.9.0 - Links from a wiki page to other wiki pages should be rewritten as expected - Add option to project to only allow merge requests to be merged if the build succeeds (Rui Santos) - Added navigation shortcuts to the project pipelines, milestones, builds and forks page. !4393 - - Fix changing issue state columns in milestone view - Fix issues filter when ordering by milestone - Disable SAML account unlink feature - Added artifacts:when to .gitlab-ci.yml - this requires GitLab Runner 1.3 diff --git a/app/assets/javascripts/milestone.js.coffee b/app/assets/javascripts/milestone.js.coffee index 0fd64e9384e..a19e68b39e2 100644 --- a/app/assets/javascripts/milestone.js.coffee +++ b/app/assets/javascripts/milestone.js.coffee @@ -4,18 +4,10 @@ class @Milestone type: "PUT" url: issue_url data: data - success: (data) -> - if data.saved == true - if data.assignee_avatar_url - img_tag = $('') - img_tag.attr('src', data.assignee_avatar_url) - img_tag.addClass('avatar s16') - $(li).find('.assignee-icon').html(img_tag) - else - $(li).find('.assignee-icon').html('') - $(li).effect 'highlight' - else - new Flash("Issue update failed", 'alert') + success: (_data) => + @successCallback(_data, li) + error: (data) -> + new Flash("Issue update failed", 'alert') dataType: "json" @sortIssues: (data) -> @@ -25,9 +17,10 @@ class @Milestone type: "PUT" url: sort_issues_url data: data - success: (data) -> - if data.saved != true - new Flash("Issues update failed", 'alert') + success: (_data) => + @successCallback(_data) + error: -> + new Flash("Issues update failed", 'alert') dataType: "json" @sortMergeRequests: (data) -> @@ -37,9 +30,10 @@ class @Milestone type: "PUT" url: sort_mr_url data: data - success: (data) -> - if data.saved != true - new Flash("MR update failed", 'alert') + success: (_data) => + @successCallback(_data) + error: (data) -> + new Flash("Issue update failed", 'alert') dataType: "json" @updateMergeRequest: (li, merge_request_url, data) -> @@ -47,20 +41,23 @@ class @Milestone type: "PUT" url: merge_request_url data: data - success: (data) -> - if data.saved == true - if data.assignee_avatar_url - img_tag = $('') - img_tag.attr('src', data.assignee_avatar_url) - img_tag.addClass('avatar s16') - $(li).find('.assignee-icon').html(img_tag) - else - $(li).find('.assignee-icon').html('') - $(li).effect 'highlight' - else - new Flash("Issue update failed", 'alert') + success: (_data) => + @successCallback(_data, li) + error: (data) -> + new Flash("Issue update failed", 'alert') dataType: "json" + @successCallback: (data, element) => + if data.assignee + img_tag = $('') + img_tag.attr('src', data.assignee.avatar_url) + img_tag.addClass('avatar s16') + $(element).find('.assignee-icon').html(img_tag) + else + $(element).find('.assignee-icon').html('') + + $(element).effect 'highlight' + constructor: -> oldMouseStart = $.ui.sortable.prototype._mouseStart $.ui.sortable.prototype._mouseStart = (event, overrideHandle, noActivation) -> @@ -81,7 +78,8 @@ class @Milestone stop: (event, ui) -> $(".issues-sortable-list").css "min-height", "0px" update: (event, ui) -> - if this == ui.item.parent()[0] + # Prevents sorting from container which element has been removed. + if $(this).find(ui.item).length > 0 data = $(this).sortable("serialize") Milestone.sortIssues(data) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 80dbabf51a5..8b8df680739 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -115,8 +115,9 @@ class Projects::IssuesController < Projects::ApplicationController render :edit end end + format.json do - render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: :saved) + render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }) end end end diff --git a/app/models/issue.rb b/app/models/issue.rb index 421fd44dffc..3c5859194b4 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -190,8 +190,4 @@ class Issue < ActiveRecord::Base def overdue? due_date.try(:past?) || false end - - def saved - valid? - end end diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml index 47b66d44e43..3c03c220ddd 100644 --- a/app/views/shared/milestones/_issuable.html.haml +++ b/app/views/shared/milestones/_issuable.html.haml @@ -21,7 +21,8 @@ = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do - render_colored_label(label) - - if assignee - = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }), - class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do - - image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '') + %span{ class: "assignee-icon" } + - if assignee + = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }), + class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do + - image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '') -- cgit v1.2.1 From 21a1fc436fe81e4bcb2fdcad73cdf7e8105ae838 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 24 Jun 2016 11:56:19 -0700 Subject: Add SMTP as default delivery method to match gitlab-org/omnibus-gitlab!826 Closes #19132 [ci skip] --- config/initializers/smtp_settings.rb.sample | 1 + 1 file changed, 1 insertion(+) diff --git a/config/initializers/smtp_settings.rb.sample b/config/initializers/smtp_settings.rb.sample index 2287a76fca7..bd37080b1c8 100644 --- a/config/initializers/smtp_settings.rb.sample +++ b/config/initializers/smtp_settings.rb.sample @@ -10,6 +10,7 @@ if Rails.env.production? Rails.application.config.action_mailer.delivery_method = :smtp + ActionMailer::Base.delivery_method = :smtp ActionMailer::Base.smtp_settings = { address: "email.server.com", port: 465, -- cgit v1.2.1 From 12c688c103114f74c2bcb5fa6125ed6f91cae945 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 24 Jun 2016 12:08:23 -0700 Subject: Update EE downgrade instructions to include JenkinsDeprecatedService Closes #19133 [ci skip] --- doc/downgrade_ee_to_ce/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/downgrade_ee_to_ce/README.md b/doc/downgrade_ee_to_ce/README.md index 3625c4191b8..a6d22e5a04a 100644 --- a/doc/downgrade_ee_to_ce/README.md +++ b/doc/downgrade_ee_to_ce/README.md @@ -44,13 +44,13 @@ to avoid getting this error, you need to remove all instances of the **Omnibus Installation** ``` -$ sudo gitlab-rails runner "Service.where(type: 'JenkinsService').delete_all" +$ sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService']).delete_all" ``` **Source Installation** ``` -$ bundle exec rails runner "Service.where(type: 'JenkinsService').delete_all" production +$ bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService']).delete_all" production ``` ## Downgrade to CE -- cgit v1.2.1 From 7627cc19897d1ff8963fde37697e6dc5d32e51ba Mon Sep 17 00:00:00 2001 From: Ruben Davila Date: Fri, 24 Jun 2016 16:20:53 -0500 Subject: Validate presence of essential params for diff rendering This will avoid application errors generated by the assumption of the presence of these params. --- app/controllers/projects/blob_controller.rb | 7 ++++ spec/controllers/projects/blob_controller_spec.rb | 40 +++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 spec/controllers/projects/blob_controller_spec.rb diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index cd8b2911674..7599fec3cdf 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -16,6 +16,7 @@ class Projects::BlobController < Projects::ApplicationController before_action :from_merge_request, only: [:edit, :update] before_action :require_branch_head, only: [:edit, :update] before_action :editor_variables, except: [:show, :preview, :diff] + before_action :validate_diff_params, only: :diff def new commit unless @repository.empty? @@ -146,4 +147,10 @@ class Projects::BlobController < Projects::ApplicationController file_content_encoding: params[:encoding] } end + + def validate_diff_params + if [:since, :to, :offset].any? { |key| params[key].blank? } + render nothing: true + end + end end diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb new file mode 100644 index 00000000000..9444a50b1ce --- /dev/null +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -0,0 +1,40 @@ +require 'rails_helper' + +describe Projects::BlobController do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + user = create(:user) + project.team << [user, :master] + + sign_in(user) + end + + describe 'GET diff' do + render_views + + def do_get(opts = {}) + params = { namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: 'master/CHANGELOG' } + get :diff, params.merge(opts) + end + + context 'when essential params are missing' do + it 'renders nothing' do + do_get + + expect(response.body).to be_blank + end + end + + context 'when essential params are present' do + it 'renders the diff content' do + do_get(since: 1, to: 5, offset: 10) + + expect(response.body).to be_present + end + end + end +end -- cgit v1.2.1 From 2fc91c48659b84375173b038b4e7957e56294aa4 Mon Sep 17 00:00:00 2001 From: Simon Welsh Date: Sun, 19 Jun 2016 09:44:50 +1000 Subject: Allow "ci skip" to be in any case --- CHANGELOG | 1 + app/models/ci/pipeline.rb | 2 +- doc/ci/yaml/README.md | 4 ++-- spec/services/create_commit_builds_service_spec.rb | 15 +++++++++++++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d32c1fd8492..eb94f963982 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,7 @@ v 8.10.0 (unreleased) - Fix changing issue state columns in milestone view - Fix user creation with stronger minimum password requirements !4054 (nathan-pmt) - Add API endpoint for a group issues !4520 (mahcsig) + - Allow [ci skip] to be in any case. !4785 (simon_w) v 8.9.1 - Fix merge requests project settings help link anchor diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 0c9a5e42eec..89434abca05 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -163,7 +163,7 @@ module Ci end def skip_ci? - git_commit_message =~ /(\[ci skip\])/ if git_commit_message + git_commit_message =~ /(\[ci skip\])/i if git_commit_message end def environments diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 1892acda29b..ca3bf37f8ad 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1034,8 +1034,8 @@ You can find the link under `/ci/lint` of your gitlab instance. ## Skipping builds -If your commit message contains `[ci skip]`, the commit will be created but the -builds will be skipped. +If your commit message contains `[ci skip]`, using any capitalization, the +commit will be created but the builds will be skipped. ## Examples diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb index deab242f45a..2255945a1ad 100644 --- a/spec/services/create_commit_builds_service_spec.rb +++ b/spec/services/create_commit_builds_service_spec.rb @@ -83,6 +83,7 @@ describe CreateCommitBuildsService, services: true do context 'when commit contains a [ci skip] directive' do let(:message) { "some message[ci skip]" } + let(:capMessage) { "some message[CI SKIP]" } before do allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } @@ -101,6 +102,20 @@ describe CreateCommitBuildsService, services: true do expect(pipeline.status).to eq("skipped") end + it "skips builds creation if there is [CI SKIP] tag in commit message" do + commits = [{ message: capMessage }] + pipeline = service.execute(project, user, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) + + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("skipped") + end + it "does not skips builds creation if there is no [ci skip] tag in commit message" do allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" } -- cgit v1.2.1 From 0550f2a5e7d8a7ed65632190b8febe2a9e01d6a0 Mon Sep 17 00:00:00 2001 From: Simon Welsh Date: Thu, 23 Jun 2016 20:09:54 +1000 Subject: Add support for "skip ci" too --- CHANGELOG | 2 +- app/models/ci/pipeline.rb | 2 +- doc/ci/yaml/README.md | 4 +-- spec/services/create_commit_builds_service_spec.rb | 33 +++++++++++++++++++++- 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index eb94f963982..e257f2cf3a2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,7 +11,7 @@ v 8.10.0 (unreleased) - Fix changing issue state columns in milestone view - Fix user creation with stronger minimum password requirements !4054 (nathan-pmt) - Add API endpoint for a group issues !4520 (mahcsig) - - Allow [ci skip] to be in any case. !4785 (simon_w) + - Allow [ci skip] to be in any case and allow [skip ci]. !4785 (simon_w) v 8.9.1 - Fix merge requests project settings help link anchor diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 89434abca05..10324bf2257 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -163,7 +163,7 @@ module Ci end def skip_ci? - git_commit_message =~ /(\[ci skip\])/i if git_commit_message + git_commit_message =~ /\[(ci skip|skip ci)\]/i if git_commit_message end def environments diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index ca3bf37f8ad..d2d1b04f893 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1034,8 +1034,8 @@ You can find the link under `/ci/lint` of your gitlab instance. ## Skipping builds -If your commit message contains `[ci skip]`, using any capitalization, the -commit will be created but the builds will be skipped. +If your commit message contains `[ci skip]` or `[skip ci]`, using any +capitalization, the commit will be created but the builds will be skipped. ## Examples diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb index 2255945a1ad..309213bd44c 100644 --- a/spec/services/create_commit_builds_service_spec.rb +++ b/spec/services/create_commit_builds_service_spec.rb @@ -83,7 +83,9 @@ describe CreateCommitBuildsService, services: true do context 'when commit contains a [ci skip] directive' do let(:message) { "some message[ci skip]" } + let(:messageFlip) { "some message[skip ci]" } let(:capMessage) { "some message[CI SKIP]" } + let(:capMessageFlip) { "some message[SKIP CI]" } before do allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } @@ -97,6 +99,21 @@ describe CreateCommitBuildsService, services: true do after: '31das312', commits: commits ) + + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("skipped") + end + + it "skips builds creation if there is [skip ci] tag in commit message" do + commits = [{ message: messageFlip }] + pipeline = service.execute(project, user, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) + expect(pipeline).to be_persisted expect(pipeline.builds.any?).to be false expect(pipeline.status).to eq("skipped") @@ -116,7 +133,21 @@ describe CreateCommitBuildsService, services: true do expect(pipeline.status).to eq("skipped") end - it "does not skips builds creation if there is no [ci skip] tag in commit message" do + it "skips builds creation if there is [SKIP CI] tag in commit message" do + commits = [{ message: capMessageFlip }] + pipeline = service.execute(project, user, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) + + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("skipped") + end + + it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" } commits = [{ message: "some message" }] -- cgit v1.2.1 From fa60c18a2ece57c39eaa172ca48dc0228a8bcb5c Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Sun, 26 Jun 2016 00:16:39 +0000 Subject: Removed unneeded word --- doc/install/requirements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 09c6211b3ab..a65ac8a5f79 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -52,7 +52,7 @@ If you have enough RAM memory and a recent CPU the speed of GitLab is mainly lim ### CPU -- 1 core works supports up to 100 users but the application can be a bit slower due to having all workers and background jobs running on the same core +- 1 core supports up to 100 users but the application can be a bit slower due to having all workers and background jobs running on the same core - **2 cores** is the **recommended** number of cores and supports up to 500 users - 4 cores supports up to 2,000 users - 8 cores supports up to 5,000 users -- cgit v1.2.1 From 81fd9149489f65a87602f56ce8267bb90c3c6324 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sat, 25 Jun 2016 20:29:24 -0400 Subject: Update CHANGELOG for 8.9.1 [ci skip] --- CHANGELOG | 53 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d32c1fd8492..4a8188ea060 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,18 +13,47 @@ v 8.10.0 (unreleased) - Add API endpoint for a group issues !4520 (mahcsig) v 8.9.1 - - Fix merge requests project settings help link anchor - - Fix GitLab project import issues related to notes and builds - - Improve performance of searching repository tags by name by using a memorized tag array - - Fix false truncated warnings with ISO-8559 files - - Eager load award emoji on notes - - Fix unwanted label unassignment when doing bulk action on issues page - - Improve performance of redacting Markdown references - - Fix 404 when accessing pipelines as guest user on public projects - - Fix mobile Safari bug where horizontal nav arrows would flicker on scroll - - Fix in auto merge when pipeline is nil - - Fix GitLab import project deleting imported file straight after being uploaded - - Fix a wrong MR status when merge_when_build_succeeds & project.only_allow_merge_if_build_succeeds are true + - Refactor labels documentation. !3347 + - Eager load award emoji on notes. !4628 + - Fix some CI wording in documentation. !4660 + - Document `GIT_STRATEGY` and `GIT_DEPTH`. !4720 + - Add documentation for the export & import features. !4732 + - Add some docs for Docker Registry configuration. !4738 + - Ensure we don't send the "access request declined" email to access requesters on project deletion. !4744 + - Display group/project access requesters separately in the admin area. !4798 + - Add documentation and examples for configuring cloud storage for registry images. !4812 + - Clarifies documentation about artifact expiry. !4831 + - Fix the Network graph links. !4832 + - Fix MR-auto-close text added to description. !4836 + - Add documentation for award emoji now that comments can be awarded with emojis. !4839 + - Fix typo in export failure email. !4847 + - Fix header vertical centering. !4170 + - Fix subsequent SAML sign ins. !4718 + - Set button label when picking an option from status dropdown. !4771 + - Prevent invalid URLs from raising exceptions in WikiLink Filter. !4775 + - Handle external issues in IssueReferenceFilter. !4789 + - Support for rendering/redacting multiple documents. !4828 + - Update Todos documentation and screenshots to include new functionality. !4840 + - Hide nav arrows by default. !4843 + - Added bottom padding to label color suggestion link. !4845 + - Use jQuery objects in ref dropdown. !4850 + - Fix GitLab project import issues related to notes and builds. !4855 + - Restrict header logo to 36px so it doesn't overflow. !4861 + - Fix unwanted label unassignment. !4863 + - Fix mobile Safari bug where horizontal nav arrows would flicker on scroll. !4869 + - Restore old behavior around diff notes to outdated discussions. !4870 + - Fix merge requests project settings help link anchor. !4873 + - Fix 404 when accessing pipelines as guest user on public projects. !4881 + - Remove width restriction for logo on sign-in page. !4888 + - Bump gitlab_git to 10.2.3 to fix false truncated warnings with ISO-8559 files. !4884 + - Apply selected value as label. !4886 + - Fix temp file being deleted after the request while importing a GitLab project. !4894 + - Fix pagination when sorting by columns with lots of ties (like priority) + - Implement Subresource Integrity for CSS and JavaScript assets. This prevents malicious assets from loading in the case of a CDN compromise. + - Fix user creation with stronger minimum password requirements !4054 (nathan-pmt) + - Add API endpoint for a group issues !4520 (mahcsig) + - Fix a wrong MR status when merge_when_build_succeeds & project.only_allow_merge_if_build_succeeds are true. !4912 + - Add SMTP as default delivery method to match gitlab-org/omnibus-gitlab!826. !4915 v 8.9.0 - Fix builds API response not including commit data -- cgit v1.2.1 From ed713c81d73b56fafd4f9228a2e2787584cdf6d7 Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Sun, 26 Jun 2016 14:59:51 +0900 Subject: Clean up some unused templates imported from GitLab CI --- app/views/ci/errors/show.haml | 2 -- app/views/ci/shared/_guide.html.haml | 13 ------------- app/views/ci/shared/_no_runners.html.haml | 7 ------- app/views/layouts/ci/_info.html.haml | 2 -- app/views/layouts/ci/_page.html.haml | 22 ---------------------- app/views/layouts/ci/notify.html.haml | 19 ------------------- 6 files changed, 65 deletions(-) delete mode 100644 app/views/ci/errors/show.haml delete mode 100644 app/views/ci/shared/_guide.html.haml delete mode 100644 app/views/ci/shared/_no_runners.html.haml delete mode 100644 app/views/layouts/ci/_info.html.haml delete mode 100644 app/views/layouts/ci/_page.html.haml delete mode 100644 app/views/layouts/ci/notify.html.haml diff --git a/app/views/ci/errors/show.haml b/app/views/ci/errors/show.haml deleted file mode 100644 index 2788112c835..00000000000 --- a/app/views/ci/errors/show.haml +++ /dev/null @@ -1,2 +0,0 @@ -%h3.error Error -= @error diff --git a/app/views/ci/shared/_guide.html.haml b/app/views/ci/shared/_guide.html.haml deleted file mode 100644 index 09e7e653521..00000000000 --- a/app/views/ci/shared/_guide.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -.bs-callout.help-callout - %h4 How to setup CI for this project - - %ol - %li - Add at least one runner to the project. - Go to #{link_to 'Runners page', runners_path(@project), target: :blank} for instructions. - %li - Put the .gitlab-ci.yml in the root of your repository. Examples can be found in - #{link_to "Configuring project (.gitlab-ci.yml)", "http://doc.gitlab.com/ci/yaml/README.html", target: :blank}. - You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} - %li - Return to this page and refresh it, it should show a new build. diff --git a/app/views/ci/shared/_no_runners.html.haml b/app/views/ci/shared/_no_runners.html.haml deleted file mode 100644 index f56c37d9b37..00000000000 --- a/app/views/ci/shared/_no_runners.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -.alert.alert-danger - %p - Now you need Runners to process your builds. - %span - Checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} to install it - - diff --git a/app/views/layouts/ci/_info.html.haml b/app/views/layouts/ci/_info.html.haml deleted file mode 100644 index 24c68a6dbf5..00000000000 --- a/app/views/layouts/ci/_info.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -- if current_user && current_user.is_admin? && Ci::Runner.count.zero? - = render 'ci/shared/no_runners' diff --git a/app/views/layouts/ci/_page.html.haml b/app/views/layouts/ci/_page.html.haml deleted file mode 100644 index 2e56d0ac6a3..00000000000 --- a/app/views/layouts/ci/_page.html.haml +++ /dev/null @@ -1,22 +0,0 @@ -.page-with-sidebar{ class: page_sidebar_class } - = render "layouts/broadcast" - .sidebar-wrapper.nicescroll{ class: nav_sidebar_class } - - - if defined?(sidebar) && sidebar - = render "layouts/ci/#{sidebar}" - - elsif current_user - = render 'layouts/nav/dashboard' - .collapse-nav - = render partial: 'layouts/collapse_button' - - if current_user - = link_to current_user, class: 'sidebar-user', title: "Profile" do - = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36' - .username - = current_user.username - .content-wrapper - = render "layouts/flash" - = render 'layouts/ci/info' - %div{ class: container_class } - .content - .clearfix - = yield diff --git a/app/views/layouts/ci/notify.html.haml b/app/views/layouts/ci/notify.html.haml deleted file mode 100644 index 270b206df5e..00000000000 --- a/app/views/layouts/ci/notify.html.haml +++ /dev/null @@ -1,19 +0,0 @@ -%html{lang: "en"} - %head - %meta{content: "text/html; charset=utf-8", "http-equiv" => "Content-Type"} - %title - GitLab CI - - %body - = yield :header - - %table{align: "left", border: "0", cellpadding: "0", cellspacing: "0", style: "padding: 10px 0;", width: "100%"} - %tr - %td{align: "left", style: "margin: 0; padding: 10px;"} - = yield - %br - %tr - %td{align: "left", style: "margin: 0; padding: 10px;"} - %p{style: "font-size:small;color:#777"} - - if @project - You're receiving this notification because you are the one who triggered a build on the #{@project.name} project. -- cgit v1.2.1 From 7773fe5b03990cc8e281db4d9ebaf4c9d168617d Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Sun, 26 Jun 2016 22:46:10 +0900 Subject: Remove docs for GitLab CI Service API --- doc/api/services.md | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/doc/api/services.md b/doc/api/services.md index ccfc0fccb7f..32d6e2dea78 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -374,40 +374,6 @@ Get Gemnasium service settings for a project. GET /projects/:id/services/gemnasium ``` -## GitLab CI - -Continuous integration server from GitLab - -### Create/Edit GitLab CI service - -Set GitLab CI service for a project. - -``` -PUT /projects/:id/services/gitlab-ci -``` - -Parameters: - -- `token` (**required**) - GitLab CI project specific token -- `project_url` (**required**) - http://ci.gitlabhq.com/projects/3 -- `enable_ssl_verification` (optional) - Enable SSL verification - -### Delete GitLab CI service - -Delete GitLab CI service for a project. - -``` -DELETE /projects/:id/services/gitlab-ci -``` - -### Get GitLab CI service settings - -Get GitLab CI service settings for a project. - -``` -GET /projects/:id/services/gitlab-ci -``` - ## HipChat Private group chat and IM -- cgit v1.2.1 From cf8a19b13cbdbade7027fb10fd3f2f67695cf1e6 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Fri, 24 Jun 2016 13:28:13 +0200 Subject: Test templates and GitLabCI parser againts each other --- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index d562d8b25ea..caf947da08e 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -1206,5 +1206,17 @@ EOT end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: dependencies parameter should be an array of strings") end end + + describe "Validate configuration templates" do + templates = Dir.glob("#{Rails.root.join('vendor/gitlab-ci-yml')}/**/*.gitlab-ci.yml") + + templates.each do |file| + it "does not return errors for #{file}" do + file = File.read(file) + + expect { GitlabCiYamlProcessor.new(file) }.not_to raise_error + end + end + end end end -- cgit v1.2.1 From f7fc352b4a54c5b86a47a5e447aefbb40558a2cd Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 21 Jun 2016 11:03:50 +0530 Subject: Add notices about disabling auth features for users with 2FA. --- doc/api/oauth2.md | 9 +++++++++ doc/api/session.md | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md index d416a826f79..31902e145f6 100644 --- a/doc/api/oauth2.md +++ b/doc/api/oauth2.md @@ -65,6 +65,13 @@ curl -H "Authorization: Bearer OAUTH-TOKEN" https://localhost:3000/api/v3/user ## Resource Owner Password Credentials +## Deprecation Notice + +1. Starting in GitLab 9.0, the Resource Owner Password Credentials will be *disabled* for users with two-factor authentication turned on. +2. These users can access the API using [personal access tokens] instead. + +--- + In this flow, a token is requested in exchange for the resource owner credentials (username and password). The credentials should only be used when there is a high degree of trust between the resource owner and the client (e.g. the client is part of the device operating system or a highly privileged application), and when other authorization grant types are not @@ -100,3 +107,5 @@ client = OAuth2::Client.new('the_client_id', 'the_client_secret', :site => "http access_token = client.password.get_token('user@example.com', 'sekret') puts access_token.token ``` + +[personal access tokens]: ./README.md#personal-access-tokens diff --git a/doc/api/session.md b/doc/api/session.md index 71e93d0bb0a..066a055702d 100644 --- a/doc/api/session.md +++ b/doc/api/session.md @@ -1,5 +1,12 @@ # Session +## Deprecation Notice + +1. Starting in GitLab 9.0, this feature will be *disabled* for users with two-factor authentication turned on. +2. These users can access the API using [personal access tokens] instead. + +--- + You can login with both GitLab and LDAP credentials in order to obtain the private token. @@ -45,3 +52,5 @@ Example response: "private_token": "9koXpg98eAheJpvBs5tK" } ``` + +[personal access tokens]: ./README.md#personal-access-tokens -- cgit v1.2.1 From 591e6c3b14b3d5ef2d8c39df30a2bde35aa1fde9 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Mon, 27 Jun 2016 12:58:05 +0200 Subject: Remove duplicate changelog entry --- CHANGELOG | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 4a8188ea060..663fb979807 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -51,7 +51,6 @@ v 8.9.1 - Fix pagination when sorting by columns with lots of ties (like priority) - Implement Subresource Integrity for CSS and JavaScript assets. This prevents malicious assets from loading in the case of a CDN compromise. - Fix user creation with stronger minimum password requirements !4054 (nathan-pmt) - - Add API endpoint for a group issues !4520 (mahcsig) - Fix a wrong MR status when merge_when_build_succeeds & project.only_allow_merge_if_build_succeeds are true. !4912 - Add SMTP as default delivery method to match gitlab-org/omnibus-gitlab!826. !4915 -- cgit v1.2.1